diff --git a/client/src/common/components/buttons/DeleteIconBtn.jsx b/client/src/common/components/buttons/DeleteIconBtn.jsx index 0e6f18132..eb4fea6b4 100644 --- a/client/src/common/components/buttons/DeleteIconBtn.jsx +++ b/client/src/common/components/buttons/DeleteIconBtn.jsx @@ -2,13 +2,13 @@ import { IconButton } from '@chakra-ui/button'; import { FiMinus } from 'react-icons/fi'; export default function DeleteIconBtn(props) { - const { clickhandler, ...rest } = props; + const { actionHandler, ...rest } = props; return ( } colorScheme='red' - onClick={clickhandler} + onClick={() => actionHandler('delete')} _focus={{ boxShadow: 'none' }} {...rest} /> diff --git a/client/src/common/components/buttons/TrashIconBtn.jsx b/client/src/common/components/buttons/TrashIconBtn.jsx new file mode 100644 index 000000000..20b9aef38 --- /dev/null +++ b/client/src/common/components/buttons/TrashIconBtn.jsx @@ -0,0 +1,16 @@ +import { IconButton } from '@chakra-ui/button'; +import { FiTrash2 } from 'react-icons/fi'; + +export default function TrashIconBtn(props) { + const { clickhandler, ...rest } = props; + return ( + } + colorScheme='red' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + ); +} diff --git a/client/src/common/components/buttons/VisibleIconBtn.jsx b/client/src/common/components/buttons/VisibleIconBtn.jsx index db06c8e34..ba2f4ec0f 100644 --- a/client/src/common/components/buttons/VisibleIconBtn.jsx +++ b/client/src/common/components/buttons/VisibleIconBtn.jsx @@ -2,14 +2,16 @@ import { IconButton } from '@chakra-ui/button'; import { FiSun } from 'react-icons/fi'; export default function VisibleIconBtn(props) { - const { clickhandler, active, ...rest } = props; + const { actionHandler, active, ...rest } = props; return ( } colorScheme='blue' variant={active ? 'solid' : 'outline'} - onClick={clickhandler} + onClick={() => + actionHandler('update', { field: 'isPublic', value: !active }) + } _focus={{ boxShadow: 'none' }} {...rest} /> diff --git a/client/src/common/components/eventTimes/EventTimes.jsx b/client/src/common/components/eventTimes/EventTimes.jsx index c680cce07..568b40b2e 100644 --- a/client/src/common/components/eventTimes/EventTimes.jsx +++ b/client/src/common/components/eventTimes/EventTimes.jsx @@ -2,7 +2,7 @@ import EditableTimer from '../../input/EditableTimer'; import { showWarningToast } from '../../helpers/toastManager'; export default function EventTimes(props) { - const { updateValues, delay, timeStart, timeEnd } = props; + const { actionHandler, delay, timeStart, timeEnd } = props; const handleValidate = (entry, v) => { // we dont inforce validation here @@ -26,14 +26,14 @@ export default function EventTimes(props) { diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx new file mode 100644 index 000000000..b9517db1b --- /dev/null +++ b/client/src/common/components/eventTimes/EventTimesVertical.jsx @@ -0,0 +1,129 @@ +import EditableTimer from '../../input/EditableTimer'; +import { showWarningToast } from '../../helpers/toastManager'; +import { useEffect, useState } from 'react'; +import { millisToMinutes, stringFromMillis } from '../../dateConfig'; + +const label = { + fontSize: '0.75em', + color: '#aaa', +}; + +const TimesDelayed = (props) => { + const { handleValidate, actionHandler, delay, timeStart, timeEnd } = props; + + const scheduledStart = stringFromMillis(timeStart, false); + + const scheduledEnd = stringFromMillis(timeEnd, false); + + return ( + <> + + Start {scheduledStart} + + + + End {scheduledEnd} + + + Duration + + + ); +}; + +const Times = (props) => { + const { handleValidate, actionHandler, timeStart, timeEnd, duration } = props; + + return ( + <> + Start + + End + + Duration (min) + + + ); +}; + +export default function EventTimesVertical(props) { + const { delay, timeStart, timeEnd } = props; + const [duration, setDuration] = useState( + millisToMinutes(timeEnd - timeStart) || 0 + ); + useEffect(() => { + setDuration(millisToMinutes(timeEnd - timeStart)); + }, [timeStart, timeEnd]); + + const handleValidate = (entry, v) => { + // we dont enforce validation here + + if (v == null || timeStart == null || timeEnd == null) return true; + if (timeStart === 0) return true; + + let validate = { value: true, catch: '' }; + if (entry === 'timeStart' && v > timeEnd) + validate.catch = 'Start time later than end time'; + else if (entry === 'timeEnd' && v < timeStart) + validate.catch = 'End time earlier than start time'; + + if (validate.catch !== '') + showWarningToast('Time Input Warning', validate.catch); + return validate.value; + }; + + return (delay != null) & (delay > 0) ? ( + + ) : ( + + ); +} diff --git a/client/src/common/dateConfig.js b/client/src/common/dateConfig.js index a89197bc0..857812c4f 100644 --- a/client/src/common/dateConfig.js +++ b/client/src/common/dateConfig.js @@ -8,11 +8,12 @@ export const stringFromMillis = ( delim = ':', ifNull = '...' ) => { - if (ms === null) return ifNull; + if (ms === null || isNaN(ms)) return ifNull; const showWith0 = (value) => (value < 10 ? `0${value}` : value); const hours = showWith0(Math.floor(((ms / (1000 * 60 * 60)) % 60) % 24)); const minutes = showWith0(Math.floor((ms / (1000 * 60)) % 60)); const seconds = showWith0(Math.floor((ms / 1000) % 60)); + return showSeconds ? `${ parseInt(hours) ? `${hours}${delim}` : `00${delim}` diff --git a/client/src/common/input/TimeInput.jsx b/client/src/common/input/DelayInput.jsx similarity index 83% rename from client/src/common/input/TimeInput.jsx rename to client/src/common/input/DelayInput.jsx index 9ac84a6ef..8a1fba8e9 100644 --- a/client/src/common/input/TimeInput.jsx +++ b/client/src/common/input/DelayInput.jsx @@ -13,7 +13,7 @@ const inputProps = { textAlign: 'center', }; -export default function TimeInput(props) { +export default function DelayInput(props) { const [value, setValue] = useState(props.value); useEffect(() => { @@ -29,9 +29,10 @@ export default function TimeInput(props) { if (val === props.value) return; if (val === '') setValue(0); if (val >= 0 && val <= 60) { - props.submitHandler(val); + // convert to ms and updates + props.actionHandler('update', { field: 'duration', value: val * 60000 }); } else { - setValue(60); + props.actionHandler('update', { field: 'duration', value: 3600000 }); } }; diff --git a/client/src/common/input/DelayValue.jsx b/client/src/common/input/DelayValue.jsx deleted file mode 100644 index 0d6879712..000000000 --- a/client/src/common/input/DelayValue.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { millisToMinutes } from '../dateConfig'; -import style from '../../features/editors/list/Block.module.css'; - -export default function DelayValue(props) { - const { delay } = props; - const delayed = delay > 0; - return ( -
- {delayed && {`+ ${millisToMinutes(delay)}`}} -
- ); -} diff --git a/client/src/common/input/EditableText.jsx b/client/src/common/input/EditableText.jsx index 0e13627ad..73739d6c2 100644 --- a/client/src/common/input/EditableText.jsx +++ b/client/src/common/input/EditableText.jsx @@ -4,7 +4,7 @@ import style from './EditableText.module.css'; export default function EditableText(props) { const { label, defaultValue, placeholder, submitHandler } = props; - const [text, setText] = useState(''); + const [text, setText] = useState(defaultValue || ''); useEffect(() => { if (defaultValue == null) setText(''); @@ -19,14 +19,11 @@ export default function EditableText(props) { const handleChange = (val) => { if (val.length < 40) setText(val); - console.log(val.length); }; return (
- - {label} - + {label} handleChange(v)} onSubmit={(v) => handleSubmit(v)} @@ -34,8 +31,11 @@ export default function EditableText(props) { placeholder={placeholder} className={style.inline} > - - + +
); diff --git a/client/src/common/input/EditableText.module.css b/client/src/common/input/EditableText.module.css index 735435bca..d1edacc92 100644 --- a/client/src/common/input/EditableText.module.css +++ b/client/src/common/input/EditableText.module.css @@ -1,24 +1,16 @@ -.title, -.titleUnderlined { +.title { padding-left: 1em; - font-size: 0.8em; + font-size: 0.75em; color: #aaa; display: inline-block; width: 6em; - cursor: default; -} - -.titleUnderlined { - border-bottom: 1px solid rgba(255, 255, 255, 0.15); } .block { display: 'block'; - overflow: hidden; - max-width: 100%; + overflow-x: hidden; } .inline { display: inline; - width: max-content; } diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx index 26fd1e835..2c771dbbf 100644 --- a/client/src/common/input/EditableTimer.jsx +++ b/client/src/common/input/EditableTimer.jsx @@ -9,7 +9,7 @@ import { showErrorToast } from '../helpers/toastManager'; import style from './EditableTimer.module.css'; export default function EditableTimer(props) { - const { name, updateValues, time, delay, validate } = props; + const { name, actionHandler, time, delay, validate } = props; const [value, setValue] = useState(''); // prepare time fields @@ -46,7 +46,7 @@ export default function EditableTimer(props) { if (!validate(name, millis)) return false; // update entry - updateValues(name, millis); + actionHandler('update', { field: name, value: millis }); return true; }; @@ -56,19 +56,17 @@ export default function EditableTimer(props) { }; return ( -
- showOriginal()} - onEdit={() => showOriginal} - onChange={(v) => setValue(v)} - onSubmit={(v) => validateValue(v)} - value={value} - placeholder='--:--' - className={delay > 0 ? style.delayedEditable : style.editable} - > - - - -
+ showOriginal} + onEdit={() => showOriginal} + onChange={(v) => setValue(v)} + onSubmit={(v) => validateValue(v)} + value={value} + placeholder='--:--' + className={delay > 0 ? style.delayedEditable : style.editable} + > + + + ); } diff --git a/client/src/common/input/EditableTimer.module.css b/client/src/common/input/EditableTimer.module.css index 4cd23a677..d10c24948 100644 --- a/client/src/common/input/EditableTimer.module.css +++ b/client/src/common/input/EditableTimer.module.css @@ -1,15 +1,13 @@ -.time { +.editable, +.delayedEditable { background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 4px; width: 5em; height: fit-content; -} -.editable, -.delayedEditable { text-align: center; border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.05); } .delayedEditable { diff --git a/client/src/features/control/MessageControl.jsx b/client/src/features/control/MessageControl.jsx index 4a95d6b12..2e549df34 100644 --- a/client/src/features/control/MessageControl.jsx +++ b/client/src/features/control/MessageControl.jsx @@ -94,7 +94,7 @@ export default function MessageControl() { messageControl('toggle-pres-visible')} + actionHandler={() => messageControl('toggle-pres-visible')} {...inputProps} /> @@ -114,7 +114,7 @@ export default function MessageControl() { messageControl('toggle-publ-visible')} + actionHandler={() => messageControl('toggle-publ-visible')} {...inputProps} /> @@ -135,7 +135,7 @@ export default function MessageControl() { messageControl('toggle-lower-visible')} + actionHandler={() => messageControl('toggle-lower-visible')} {...inputProps} /> diff --git a/client/src/features/editors/list/ActionButtons.jsx b/client/src/features/editors/list/ActionButtons.jsx index 316c059de..8c1d2bff4 100644 --- a/client/src/features/editors/list/ActionButtons.jsx +++ b/client/src/features/editors/list/ActionButtons.jsx @@ -3,7 +3,7 @@ import { IconButton } from '@chakra-ui/button'; import { FiZap, FiPlus, FiMinusCircle, FiClock } from 'react-icons/fi'; export default function ActionButtons(props) { - const { showAdd, showDelay, showBlock } = props; + const { showAdd, showDelay, showBlock, actionHandler } = props; const menuStyle = { color: '#000000', @@ -25,7 +25,7 @@ export default function ActionButtons(props) { } - onClick={props.addHandler} + onClick={() => actionHandler('event')} isDisabled={!showAdd} > Event next @@ -33,14 +33,14 @@ export default function ActionButtons(props) { } - onClick={props.delayHandler} + onClick={() => actionHandler('delay')} isDisabled={!showDelay} > Delay next } - onClick={props.blockHandler} + onClick={() => actionHandler('block')} isDisabled={!showBlock} > Block next diff --git a/client/src/features/editors/list/Block.module.css b/client/src/features/editors/list/Block.module.css deleted file mode 100644 index 3161dad70..000000000 --- a/client/src/features/editors/list/Block.module.css +++ /dev/null @@ -1,186 +0,0 @@ -/* ============= COMMON ============= */ - -.eventRow, -.eventRowActive, -.block, -.delay { - position: relative; - margin: 0.2em 0; - padding: 0.2em 0.5em; - - width: 100%; - border-radius: 8px; - font-size: 15px; - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.block, -.delay { - box-sizing: border-box; - display: flex; - gap: 1em; -} - -.actionOverlay { - display: flex; - flex-direction: row; - padding-top: 0.2em; - gap: 0.5em; - align-content: center; - align-self: flex-start; - - opacity: 0.8; - transition: linear 0.1s; -} - -.eventRow:hover > .actionOverlay, -.eventRowActive:hover > .actionOverlay, -.block:hover > .actionOverlay, -.delay:hover > .actionOverlay { - opacity: 1; - transition: linear 0.1s; -} - -/* NA */ -.actionOverlay:hover { - opacity: 1; -} -/* ============= EVENT ITEM ============= */ - -.eventRow, -.eventRowActive { - display: grid; - grid-template-columns: 2em 2em auto auto 1fr auto; - - justify-content: center; - align-content: center; - align-items: baseline; - gap: 0.5em; -} - -.eventRow { - background-color: rgba(255, 255, 255, 0.02); -} - -.eventRowActive { - background-color: rgba(255, 255, 255, 0.36); - background: linear-gradient( - 180deg, - #00000000 10%, - #4bffab10 89%, - #227c52aa 90%, - #4bffabcc 100% - ); -} - -.drag { - color: rgba(255, 255, 255, 0.37); - align-self: center; -} - -.indicators { - display: flex; - flex-direction: column; - align-self: flex-start; - font-size: 0.75em; - overflow: hidden; - min-width: 0; - justify-content: center; -} - -.next, -.nextDisabled { - width: max-content; - padding: 0 0.2em; - color: #4bffab; - transition: 0.3s; -} - -.next { - opacity: 0.8; -} - -.nextDisabled { - opacity: 0.05; -} - -.delayValue { - color: #d69e2eaa; - padding: 0 0.2em; - text-align: center; - align-self: flex-start; - width: max-content; -} - -.next:after, -.delayValue:after { - content: '\200b'; -} - -.titleContainer, -.detailedContainer { - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 4px; -} - -.rowDetailed { - display: inline-flex; - position: relative; - align-self: flex-start; -} - -.titleContainer, -.detailedContainer { - width: 90%; - display: block; -} - -.more { - position: absolute; - right: 0.25em; - top: 0.5em; -} - -/* ============= BLOCK BLOCK ============= */ -.block { - background-color: #805ad5; - background: linear-gradient( - 0deg, - rgba(107, 70, 193, 0.4) 0%, - rgba(128, 90, 213, 0.4) 20%, - rgba(107, 70, 193, 0.15) 21% - ); - - display: grid; - grid-template-columns: 2em auto; - align-content: center; - align-items: baseline; - gap: 0.5em; -} - -.block > .drag { - justify-self: start; -} - -.block > .actionOverlay { - justify-self: end; -} - -/* ============= DELAY BLOCK ============= */ -.delay { - background-color: #ecc94b; - background: linear-gradient( - 180deg, - rgba(214, 158, 46, 0.4) 0%, - rgba(236, 201, 75, 0.4) 20%, - rgba(214, 158, 46, 0.07) 21% - ); - - display: grid; - grid-template-columns: 2em 1fr auto; - justify-content: center; - align-content: center; - align-items: baseline; - gap: 0.5em; -} diff --git a/client/src/features/editors/list/BlockBlock.jsx b/client/src/features/editors/list/BlockBlock.jsx index ee856ea77..83851ba2e 100644 --- a/client/src/features/editors/list/BlockBlock.jsx +++ b/client/src/features/editors/list/BlockBlock.jsx @@ -2,20 +2,10 @@ import { Draggable } from 'react-beautiful-dnd'; import { FiMoreVertical } from 'react-icons/fi'; import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn'; import ActionButtons from './ActionButtons'; -import style from './Block.module.css'; +import style from './BlockBlock.module.css'; export default function BlockBlock(props) { - const { eventsHandler, index, data } = props; - - const addHandler = () => { - eventsHandler('add', { type: 'event', order: index + 1 }); - }; - const deleteHandler = () => { - eventsHandler('delete', data.id); - }; - const delayHandler = () => { - eventsHandler('add', { type: 'delay', order: index + 1 }); - }; + const { index, data, actionHandler } = props; return ( @@ -29,13 +19,8 @@ export default function BlockBlock(props) {
- - + +
)} diff --git a/client/src/features/editors/list/BlockBlock.module.css b/client/src/features/editors/list/BlockBlock.module.css new file mode 100644 index 000000000..eadc8bcae --- /dev/null +++ b/client/src/features/editors/list/BlockBlock.module.css @@ -0,0 +1,58 @@ +/* ============= COMMON ============= */ + +.block { + margin: 0.2em 0; + padding: 0.2em 0.5em; + box-sizing: border-box; + + width: 100%; + border-radius: 8px; + font-size: 15px; + border: 1px solid rgba(255, 255, 255, 0.05); + + display: grid; + grid-template-columns: 2em auto; + align-content: center; + align-items: baseline; + gap: 0.5em; + + background-color: #805ad5; + background: linear-gradient( + 0deg, + rgba(107, 70, 193, 0.4) 0%, + rgba(128, 90, 213, 0.4) 20%, + rgba(107, 70, 193, 0.15) 21% + ); +} + +/* ================ DRAG ================ */ +.drag { + color: rgba(255, 255, 255, 0.67); + align-self: center; + justify-self: start; +} + +/* ============== ACTION ================ */ +.actionOverlay { + display: flex; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-self: flex-start; + padding-top: 0.2em; + gap: 0.5em; + opacity: 0.8; + transition: linear 0.1s; + justify-self: end; +} + +.block:hover > .actionOverlay { + opacity: 1; + transition: linear 0.1s; +} + +/* NA */ +.actionOverlay:hover { + opacity: 1; +} diff --git a/client/src/features/editors/list/DelayBlock.jsx b/client/src/features/editors/list/DelayBlock.jsx index 4ee8e1ac3..cfb865729 100644 --- a/client/src/features/editors/list/DelayBlock.jsx +++ b/client/src/features/editors/list/DelayBlock.jsx @@ -1,27 +1,14 @@ import { Draggable } from 'react-beautiful-dnd'; import { FiMoreVertical } from 'react-icons/fi'; import { millisToMinutes } from '../../../common/dateConfig'; -import TimeInput from '../../../common/input/TimeInput'; -import style from './Block.module.css'; import ActionButtons from './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'; export default function DelayBlock(props) { - const { data, eventsHandler, index } = props; - - const addHandler = () => { - eventsHandler('add', { type: 'event', order: index + 1 }); - }; - - const submitHandler = (value) => { - // convert to ms and patch - eventsHandler('patch', { id: data.id, duration: value * 1000 * 60 }); - }; - - const deleteHandler = () => { - eventsHandler('delete', data.id); - }; + const { eventsHandler, data, index, actionHandler } = props; const applyDelayHandler = () => { eventsHandler('applyDelay', { id: data.id, duration: data.duration }); @@ -29,6 +16,7 @@ export default function DelayBlock(props) { let delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined; + return ( {(provided) => ( @@ -40,11 +28,15 @@ export default function DelayBlock(props) { - +
- - + +
)} diff --git a/client/src/features/editors/list/DelayBlock.module.css b/client/src/features/editors/list/DelayBlock.module.css new file mode 100644 index 000000000..a1b226c98 --- /dev/null +++ b/client/src/features/editors/list/DelayBlock.module.css @@ -0,0 +1,66 @@ +/* ============= COMMON ============= */ + +.delay { + margin: 0.2em 0; + padding: 0.2em 0.5em; + + width: 100%; + border-radius: 8px; + font-size: 15px; + border: 1px solid rgba(255, 255, 255, 0.05); + + display: grid; + grid-template-columns: 2em 1fr auto; + grid-template-areas: 'drag inpt btns'; + justify-content: center; + align-content: center; + align-items: baseline; + gap: 0.5em; + + background-color: #ecc94b; + background: linear-gradient( + 180deg, + rgba(214, 158, 46, 0.4) 0%, + rgba(236, 201, 75, 0.4) 20%, + rgba(214, 158, 46, 0.07) 21% + ); +} + +/* ================ DRAG ================ */ + +.drag { + grid-area: drag; + color: rgba(255, 255, 255, 0.67); + align-self: center; +} + +/* =============== INPUT ================ */ + +.input { + grid-area: inpt; +} + +/* ============== ACTION ================ */ + +.actionOverlay { + grid-area: btns; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-self: flex-start; + padding-top: 0.2em; + gap: 0.5em; + opacity: 0.8; + transition: linear 0.1s; +} + +.delay:hover > .actionOverlay { + opacity: 1; + transition: linear 0.1s; +} + +/* NA */ +.actionOverlay:hover { + opacity: 1; +} diff --git a/client/src/features/editors/list/EventBlock copy.jsx b/client/src/features/editors/list/EventBlock copy.jsx new file mode 100644 index 000000000..e5b9926d8 --- /dev/null +++ b/client/src/features/editors/list/EventBlock copy.jsx @@ -0,0 +1,188 @@ +import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { showErrorToast } from '../../../common/helpers/toastManager'; +import { Draggable } from 'react-beautiful-dnd'; +import EventTimes from '../../../common/components/eventTimes/EventTimes'; +// import common from './Block.module.css'; +import style from './EventBlock.module.css'; +import EditableText from '../../../common/input/EditableText'; +import DelayValue from '../../../common/input/DelayValue'; +import ActionButtons from './ActionButtons'; +import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn'; +import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn'; + +const areEqual = (prevProps, nextProps) => { + return ( + prevProps.data.revision === nextProps.data.revision && + prevProps.selected === nextProps.selected && + prevProps.next === nextProps.next && + prevProps.index === nextProps.index && + prevProps.delay === nextProps.delay + ); +}; + +const EventBlock = (props) => { + const { data, selected, next, delay, index, eventsHandler } = props; + + const [more, setMore] = useState(true); + const [visible, setVisible] = useState(data.isPublic || false); + + // Set visibility indicator + useEffect(() => { + setVisible(data.isPublic); + }, [data.isPublic]); + + // Create / delete new events + const actionHandler = (action) => { + switch (action) { + case 'event': + eventsHandler('add', { type: 'event', order: index + 1 }); + break; + case 'delay': + eventsHandler('add', { type: 'delay', order: index + 1 }); + break; + case 'block': + eventsHandler('add', { type: 'block', order: index + 1 }); + break; + case 'delete': + eventsHandler('delete', data.id); + break; + default: + break; + } + }; + + // Update data in fields + const updateValues = (field, value) => { + // validate field + if (field in data) { + // create object with new field + const newData = { id: data.id, [field]: value }; + + // request update in parent + eventsHandler('patch', newData); + } else { + showErrorToast('Field Error: ' + field); + } + }; + + const handleTitleSubmit = (v) => { + updateValues('title', v); + }; + + const handleSubtitleSubmit = (v) => { + updateValues('subtitle', v); + }; + + const handlePresenterSubmit = (v) => { + updateValues('presenter', v); + }; + + const handleNoteSubmit = (v) => { + updateValues('note', v); + }; + + const handleVisibleToggle = useCallback(() => { + const viz = !data.isPublic || !visible; + updateValues('isPublic', viz); + }, [data.isPublic, visible, updateValues]); + + // TODO: inside useEffect() + const isSelected = selected ? style.active : ''; + const isExpanded = more ? style.expanded : style.collapsed; + const classSelect = ` ${style.event} ${isExpanded} ${isSelected}`; + + return ( + + {(provided) => ( +
+ + + +
+
Next
+ +
+ + {more &&
Start Time
} + {more &&
End Time
} + {more &&
Duration
} + +
+ {more ? ( +
+ + + + +
+ ) : ( +
+ +
+ )} +
setMore(!more)}> + {more ? : } +
+
+
+ + + +
+
+ )} +
+ ); +}; + +export default memo(EventBlock, areEqual); +// export default EventBlock; diff --git a/client/src/features/editors/list/EventBlock.jsx b/client/src/features/editors/list/EventBlock.jsx index 833bb85f4..6e9d045c3 100644 --- a/client/src/features/editors/list/EventBlock.jsx +++ b/client/src/features/editors/list/EventBlock.jsx @@ -1,14 +1,15 @@ +import Icon from '@chakra-ui/icon'; import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi'; -import { memo, useEffect, useState } from 'react'; -import { showErrorToast } from '../../../common/helpers/toastManager'; +import { memo, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import EventTimes from '../../../common/components/eventTimes/EventTimes'; -import style from './Block.module.css'; +import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical'; import EditableText from '../../../common/input/EditableText'; -import DelayValue from '../../../common/input/DelayValue'; import ActionButtons from './ActionButtons'; import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn'; import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn'; +import { millisToMinutes } from '../../../common/dateConfig'; +import style from './EventBlock.module.css'; const areEqual = (prevProps, nextProps) => { return ( @@ -20,147 +21,181 @@ const areEqual = (prevProps, nextProps) => { ); }; +const ExpandedBlock = (props) => { + const { provided, data, next, delay, delayValue, actionHandler } = props; + + return ( + <> + + + + +
+ Next + {delayValue != null && ( + + {delayValue} + )} +
+
+ +
+ +
+ + actionHandler('update', { field: 'title', value: v }) + } + /> + + actionHandler('update', { field: 'presenter', value: v }) + } + /> + + actionHandler('update', { field: 'subtitle', value: v }) + } + /> + + actionHandler('update', { field: 'note', value: v }) + } + /> +
+ props.setExpanded(false)} + /> +
+ + + +
+ + ); +}; + +const CollapsedBlock = (props) => { + const { provided, data, next, delay, delayValue, actionHandler } = props; + + return ( + <> + + + + +
+ Next + {delayValue != null && ( + + {delayValue} + )} +
+ +
+ + actionHandler('update', { field: 'title', value: v }) + } + /> +
+ props.setExpanded(true)} + /> +
+ + +
+ + ); +}; + const EventBlock = (props) => { - const { data, selected, next, delay, index, eventsHandler } = props; + const { data, selected, delay, index, actionHandler } = props; - const [more, setMore] = useState(true); - const [visible, setVisible] = useState(data.isPublic || false); + const [expanded, setExpanded] = useState(true); - // Set visibility indicator - useEffect(() => { - setVisible(data.isPublic); - }, [data.isPublic]); + // TODO: should this go inside useEffect() + // Would I then need to add this to state? + const isSelected = selected ? style.active : ''; + const isExpanded = expanded ? style.expanded : style.collapsed; + const classSelect = `${style.event} ${isExpanded} ${isSelected}`; - const updateValues = (field, value) => { - // validate field - if (field in data) { - // create object with new field - const newData = { id: data.id, [field]: value }; - - // request update in parent - eventsHandler('patch', newData); - } else { - showErrorToast('Field Error: ' + field); - } - }; - - const addHandler = () => { - eventsHandler('add', { type: 'event', order: index + 1 }); - }; - const delayHandler = () => { - eventsHandler('add', { type: 'delay', order: index + 1 }); - }; - const blockHandler = () => { - eventsHandler('add', { type: 'block', order: index + 1 }); - }; - const deleteHandler = () => { - eventsHandler('delete', data.id); - }; - - const handleTitleSubmit = (v) => { - updateValues('title', v); - }; - - const handleSubtitleSubmit = (v) => { - updateValues('subtitle', v); - }; - - const handlePresenterSubmit = (v) => { - updateValues('presenter', v); - }; - - const handleNoteSubmit = (v) => { - updateValues('note', v); - }; - - const handleVisibleToggle = () => { - let viz = !data.isPublic || !visible; - updateValues('isPublic', viz); - }; + // Calculate delay in min + const delayValue = delay > 0 ? millisToMinutes(delay) : null; return ( {(provided) => (
- - - -
-
Next
- -
- -
- {more ? ( -
- - - - -
- ) : ( -
- -
- )} -
setMore(!more)}> - {more ? : } -
-
-
- - - -
+ )}
)}
@@ -168,4 +203,3 @@ const EventBlock = (props) => { }; export default memo(EventBlock, areEqual); -// export default EventBlock; diff --git a/client/src/features/editors/list/EventBlock.module.css b/client/src/features/editors/list/EventBlock.module.css new file mode 100644 index 000000000..afe089a21 --- /dev/null +++ b/client/src/features/editors/list/EventBlock.module.css @@ -0,0 +1,185 @@ +/* ============= COMMON ============= */ +.event { + margin: 0.2em 0; + padding: 0.2em 0.5em; + box-sizing: border-box; + + width: 100%; + border-radius: 8px; + font-size: 15px; + border: 1px solid rgba(255, 255, 255, 0.05); + + display: grid; + gap: 0.5em; + + background-color: rgba(255, 255, 255, 0.02); +} + +.active { + background-color: rgba(255, 255, 255, 0.36); + background: linear-gradient( + 180deg, + #00000000 10%, + #4bffab10 97%, + #227c52aa 98%, + #4bffabcc 100% + ); +} + +.collapsed { + grid-template-columns: 2em 2em auto auto 1fr auto 3.7em; + grid-template-areas: 'drag indi time time text more btns'; + justify-content: center; + align-items: baseline; +} + +.expanded { + grid-template-columns: 2em 2em 2em auto 1fr auto 3.7em; + grid-template-rows: repeat(4, 1fr); + grid-template-areas: + 'drag indi time time text more btns' + '.... indi time time text .... btns' + '.... indi time time text .... btns' + '.... indi time time text .... btns'; + justify-content: center; +} + +/* ================ DRAG ================ */ + +.drag { + grid-area: drag; + color: rgba(255, 255, 255, 0.67); + align-self: center; +} + +/* ============= INDICATORS ============= */ +.indicators { + grid-area: indi; + display: flex; + flex-direction: column; + align-self: flex-start; + font-size: 0.75em; + overflow: hidden; + min-width: 0; + justify-content: center; +} + +.next, +.nextDisabled { + width: max-content; + padding: 0 0.2em; + color: #4bffab; + transition: 0.3s; +} + +.next { + opacity: 0.8; +} + +.nextDisabled { + opacity: 0.05; +} + +.delayValue { + color: #d69e2eaa; + padding: 0 0.2em; + text-align: center; + align-self: flex-start; + width: max-content; +} + +.next:after, +.delayValue:after { + content: '\200b'; +} +/* =============== TIMES ================ */ + +.time { + grid-area: time; +} + +.start { + grid-area: star; +} + +.end { + grid-area: end; +} + +.duration { + grid-area: dura; +} + +.timeExpanded { + grid-area: time; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: flex-start; + justify-items: flex-start; + + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 4px; + padding: 0 0.5em 0.5em 0.5em; +} + +.label { + font-size: 0.75em; + color: #aaa; +} + +.calculatedTime { + grid-area: time; + font-size: 0.75em; +} + +/* =============== TITLES =============== */ + +.titleContainer { + grid-area: text; + display: inline-flex; + position: relative; + align-self: flex-start; + height: 100%; + + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 4px; + width: 100%; + display: block; +} +/* ================ MORE ================ */ + +.more { + cursor: pointer; +} +/* ============== ACTION ================ */ + +.actionOverlay { + grid-area: btns; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-self: flex-start; + padding-top: 0.2em; + gap: 0.5em; + opacity: 0.8; + transition: linear 0.1s; +} + +.expanded > .actionOverlay { + flex-direction: column; + justify-self: flex-end; +} + +.event:hover > .actionOverlay { + opacity: 1; + transition: linear 0.1s; +} + +/* NA */ +.actionOverlay:hover { + opacity: 1; +} diff --git a/client/src/features/editors/list/EventListItem.jsx b/client/src/features/editors/list/EventListItem.jsx index caec07a6f..0a6bd8b4a 100644 --- a/client/src/features/editors/list/EventListItem.jsx +++ b/client/src/features/editors/list/EventListItem.jsx @@ -1,6 +1,7 @@ import DelayBlock from './DelayBlock'; import BlockBlock from './BlockBlock'; import EventBlock from './EventBlock'; +import { showErrorToast } from '../../../common/helpers/toastManager'; export default function EventListItem(props) { const { @@ -14,6 +15,46 @@ export default function EventListItem(props) { ...rest } = props; + // Create / delete new events + const actionHandler = (action, payload) => { + switch (action) { + case 'event': + eventsHandler('add', { type: 'event', order: index + 1 }); + break; + case 'delay': + eventsHandler('add', { type: 'delay', order: index + 1 }); + break; + case 'block': + eventsHandler('add', { type: 'block', order: index + 1 }); + break; + case 'delete': + eventsHandler('delete', data.id); + break; + case 'update': + // Handles and filters update requests + const { field, value } = payload; + if (field === 'durationOverride') { + // duration defines timeEnd + let end = (data.timeStart += value); + const newData = { id: data.id, timeEnd: end }; + + // request update in parent + eventsHandler('patch', newData); + } else if (field in data) { + // create object with new field + const newData = { id: data.id, [field]: value }; + + // request update in parent + eventsHandler('patch', newData); + } else { + showErrorToast('Field Error: ' + field); + } + break; + default: + break; + } + }; + switch (type) { case 'event': return ( @@ -22,17 +63,22 @@ export default function EventListItem(props) { data={data} selected={selected} next={next} - eventsHandler={eventsHandler} + actionHandler={actionHandler} delay={delay} /> ); case 'block': return ( - + ); case 'delay': return ( - + ); default: break;