major: event block redesign

This commit is contained in:
cv
2021-05-10 21:31:16 +02:00
parent 3f598f3a7a
commit d24fb406af
23 changed files with 921 additions and 428 deletions
@@ -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 (
<IconButton
size={props.size || 'xs'}
icon={<FiMinus />}
colorScheme='red'
onClick={clickhandler}
onClick={() => actionHandler('delete')}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
@@ -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 (
<IconButton
size={props.size || 'xs'}
icon={<FiTrash2 />}
colorScheme='red'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
);
}
@@ -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 (
<IconButton
size={props.size || 'xs'}
icon={<FiSun />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
onClick={() =>
actionHandler('update', { field: 'isPublic', value: !active })
}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
@@ -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) {
<EditableTimer
name='timeStart'
validate={handleValidate}
updateValues={updateValues}
actionHandler={actionHandler}
time={timeStart}
delay={delay}
/>
<EditableTimer
name='timeEnd'
validate={handleValidate}
updateValues={updateValues}
actionHandler={actionHandler}
time={timeEnd}
delay={delay}
/>
@@ -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 (
<>
<span style={label}>
Start <span>{scheduledStart}</span>
</span>
<EditableTimer
name='timeStart'
validate={handleValidate}
actionHandler={actionHandler}
time={timeStart}
delay={delay}
/>
<span style={label}>
End <span>{scheduledEnd}</span>
</span>
<EditableTimer
name='timeEnd'
validate={handleValidate}
actionHandler={actionHandler}
time={timeEnd}
delay={delay}
/>
<span style={label}>Duration</span>
<EditableTimer
name='duration'
validate={handleValidate}
actionHandler={actionHandler}
time={timeEnd - timeStart}
delay={0}
/>
</>
);
};
const Times = (props) => {
const { handleValidate, actionHandler, timeStart, timeEnd, duration } = props;
return (
<>
<span style={label}>Start</span>
<EditableTimer
name='timeStart'
validate={handleValidate}
actionHandler={actionHandler}
time={timeStart}
delay={0}
/>
<span style={label}>End</span>
<EditableTimer
name='timeEnd'
validate={handleValidate}
actionHandler={actionHandler}
time={timeEnd}
delay={0}
/>
<span style={label}>Duration (min)</span>
<EditableTimer
name='durationOverride'
validate={handleValidate}
actionHandler={actionHandler}
time={timeEnd - timeStart}
delay={0}
/>
</>
);
};
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) ? (
<TimesDelayed
handleValidate={handleValidate}
actionHandler={props.actionHandler}
delay={delay}
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
/>
) : (
<Times
handleValidate={handleValidate}
actionHandler={props.actionHandler}
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
/>
);
}
+2 -1
View File
@@ -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}`
@@ -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 });
}
};
-12
View File
@@ -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 (
<div className={style.delayValue}>
{delayed && <span>{`+ ${millisToMinutes(delay)}`}</span>}
</div>
);
}
+7 -7
View File
@@ -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 (
<div className={style.block}>
<span className={props.underlined ? style.titleUnderlined : style.title}>
{label}
</span>
<span className={style.title}>{label}</span>
<Editable
onChange={(v) => handleChange(v)}
onSubmit={(v) => handleSubmit(v)}
@@ -34,8 +31,11 @@ export default function EditableText(props) {
placeholder={placeholder}
className={style.inline}
>
<EditablePreview />
<EditableInput />
<EditablePreview
color={text === '' ? '#666' : 'inherit'}
maxWidth='20em'
/>
<EditableInput overflowX='hidden' maxWidth='20em' />
</Editable>
</div>
);
@@ -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;
}
+14 -16
View File
@@ -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 (
<div className={style.time}>
<Editable
onFocus={() => showOriginal()}
onEdit={() => showOriginal}
onChange={(v) => setValue(v)}
onSubmit={(v) => validateValue(v)}
value={value}
placeholder='--:--'
className={delay > 0 ? style.delayedEditable : style.editable}
>
<EditablePreview />
<EditableInput type='time' min='00:00' max='23:59' />
</Editable>
</div>
<Editable
onFocus={() => showOriginal}
onEdit={() => showOriginal}
onChange={(v) => setValue(v)}
onSubmit={(v) => validateValue(v)}
value={value}
placeholder='--:--'
className={delay > 0 ? style.delayedEditable : style.editable}
>
<EditablePreview />
<EditableInput type='time' min='00:00' max='23:59' />
</Editable>
);
}
@@ -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 {
@@ -94,7 +94,7 @@ export default function MessageControl() {
</Editable>
<VisibleIconBtn
active={pres.visible || undefined}
clickhandler={() => messageControl('toggle-pres-visible')}
actionHandler={() => messageControl('toggle-pres-visible')}
{...inputProps}
/>
</div>
@@ -114,7 +114,7 @@ export default function MessageControl() {
</Editable>
<VisibleIconBtn
active={publ.visible || undefined}
clickhandler={() => messageControl('toggle-publ-visible')}
actionHandler={() => messageControl('toggle-publ-visible')}
{...inputProps}
/>
</div>
@@ -135,7 +135,7 @@ export default function MessageControl() {
<VisibleIconBtn
active={lower.visible || undefined}
clickhandler={() => messageControl('toggle-lower-visible')}
actionHandler={() => messageControl('toggle-lower-visible')}
{...inputProps}
/>
</div>
@@ -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) {
<MenuList style={menuStyle}>
<MenuItem
icon={<FiPlus />}
onClick={props.addHandler}
onClick={() => actionHandler('event')}
isDisabled={!showAdd}
>
Event next
@@ -33,14 +33,14 @@ export default function ActionButtons(props) {
<MenuItem
icon={<FiClock />}
onClick={props.delayHandler}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
Delay next
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={props.blockHandler}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Block next
@@ -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;
}
@@ -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 (
<Draggable key={data.id} draggableId={data.id} index={index}>
@@ -29,13 +19,8 @@ export default function BlockBlock(props) {
<FiMoreVertical />
</span>
<div className={style.actionOverlay}>
<DeleteIconBtn clickhandler={deleteHandler} />
<ActionButtons
showAdd
addHandler={addHandler}
showDelay
delayHandler={delayHandler}
/>
<DeleteIconBtn actionHandler={actionHandler} />
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
</div>
</div>
)}
@@ -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;
}
+11 -19
View File
@@ -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 (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
@@ -40,11 +28,15 @@ export default function DelayBlock(props) {
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<TimeInput value={delayValue} submitHandler={submitHandler} />
<DelayInput
className={style.input}
value={delayValue}
actionHandler={actionHandler}
/>
<div className={style.actionOverlay}>
<ApplyIconBtn clickhandler={applyDelayHandler} />
<DeleteIconBtn clickhandler={deleteHandler} />
<ActionButtons showAdd addHandler={addHandler} />
<DeleteIconBtn actionHandler={actionHandler} />
<ActionButtons showAdd actionHandler={actionHandler} />
</div>
</div>
)}
@@ -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;
}
@@ -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 (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div
className={classSelect}
{...provided.draggableProps}
ref={provided.innerRef}
>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<div className={next ? style.next : style.nextDisabled}>Next</div>
<DelayValue delay={delay} />
</div>
<EventTimes
updateValues={updateValues}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
delay={delay}
className={style.time}
/>
{more && <div className={style.start}>Start Time</div>}
{more && <div className={style.end}>End Time</div>}
{more && <div className={style.duration}>Duration</div>}
<div className={style.rowDetailed}>
{more ? (
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={handleTitleSubmit}
underlined
/>
<EditableText
label='Presenter'
defaultValue={data.presenter}
placeholder='Add Presenter name'
submitHandler={handlePresenterSubmit}
underlined
/>
<EditableText
label='Subtitle'
defaultValue={data.subtitle}
placeholder='Add Subtitle'
submitHandler={handleSubtitleSubmit}
underlined
/>
<EditableText
label='note'
defaultValue={data.note}
placeholder='Add note'
submitHandler={handleNoteSubmit}
underlined
/>
</div>
) : (
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={handleTitleSubmit}
isTight
/>
</div>
)}
<div className={style.more} onClick={() => setMore(!more)}>
{more ? <FiChevronUp /> : <FiChevronDown />}
</div>
</div>
<div className={style.actionOverlay}>
<VisibleIconBtn
clickhandler={handleVisibleToggle}
active={visible}
/>
<DeleteIconBtn actionHandler={actionHandler} />
<ActionButtons
showAdd
showDelay
showBlock
actionHandler={actionHandler}
/>
</div>
</div>
)}
</Draggable>
);
};
export default memo(EventBlock, areEqual);
// export default EventBlock;
+166 -132
View File
@@ -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 (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && (
<span className={style.delayValue}>+ {delayValue}</span>
)}
</div>
<div className={style.timeExpanded}>
<EventTimesVertical
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
delay={delay}
className={style.time}
/>
</div>
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={(v) =>
actionHandler('update', { field: 'title', value: v })
}
/>
<EditableText
label='Presenter'
defaultValue={data.presenter}
placeholder='Add Presenter name'
submitHandler={(v) =>
actionHandler('update', { field: 'presenter', value: v })
}
/>
<EditableText
label='Subtitle'
defaultValue={data.subtitle}
placeholder='Add Subtitle'
submitHandler={(v) =>
actionHandler('update', { field: 'subtitle', value: v })
}
/>
<EditableText
label='Note'
defaultValue={data.note}
placeholder='Add Note'
submitHandler={(v) =>
actionHandler('update', { field: 'note', value: v })
}
/>
</div>
<Icon
className={style.more}
as={FiChevronUp}
marginTop='0.2em'
gridArea='more'
onClick={() => props.setExpanded(false)}
/>
<div className={style.actionOverlay}>
<VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} />
<ActionButtons
showAdd
showDelay
showBlock
actionHandler={actionHandler}
/>
<DeleteIconBtn actionHandler={actionHandler} />
</div>
</>
);
};
const CollapsedBlock = (props) => {
const { provided, data, next, delay, delayValue, actionHandler } = props;
return (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && (
<span className={style.delayValue}>+ {delayValue}</span>
)}
</div>
<EventTimes
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
delay={delay}
className={style.time}
/>
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={(v) =>
actionHandler('update', { field: 'title', value: v })
}
/>
</div>
<Icon
className={style.more}
as={FiChevronDown}
marginTop='0.2em'
gridArea='more'
onClick={() => props.setExpanded(true)}
/>
<div className={style.actionOverlay}>
<VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} />
<ActionButtons
showAdd
showDelay
showBlock
actionHandler={actionHandler}
/>
</div>
</>
);
};
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 (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div
className={selected ? style.eventRowActive : style.eventRow}
className={classSelect}
{...provided.draggableProps}
ref={provided.innerRef}
>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<div className={next ? style.next : style.nextDisabled}>Next</div>
<DelayValue delay={delay} />
</div>
<EventTimes
updateValues={updateValues}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
delay={delay}
/>
<div className={style.rowDetailed}>
{more ? (
<div className={style.detailedContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={handleTitleSubmit}
underlined
/>
<EditableText
label='Presenter'
defaultValue={data.presenter}
placeholder='Add Presenter name'
submitHandler={handlePresenterSubmit}
underlined
/>
<EditableText
label='Subtitle'
defaultValue={data.subtitle}
placeholder='Add Subtitle'
submitHandler={handleSubtitleSubmit}
underlined
/>
<EditableText
label='note'
defaultValue={data.note}
placeholder='Add note'
submitHandler={handleNoteSubmit}
underlined
/>
</div>
) : (
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={handleTitleSubmit}
isTight
/>
</div>
)}
<div className={style.more} onClick={() => setMore(!more)}>
{more ? <FiChevronUp /> : <FiChevronDown />}
</div>
</div>
<div className={style.actionOverlay}>
<VisibleIconBtn
clickhandler={handleVisibleToggle}
active={visible}
{expanded ? (
<ExpandedBlock
provided={provided}
data={data}
next={props.next}
delay={delay}
delayValue={delayValue}
actionHandler={actionHandler}
setExpanded={setExpanded}
/>
<DeleteIconBtn clickhandler={deleteHandler} />
<ActionButtons
showAdd
addHandler={addHandler}
showDelay
delayHandler={delayHandler}
showBlock
blockHandler={blockHandler}
) : (
<CollapsedBlock
provided={provided}
data={data}
next={props.next}
delay={delay}
delayValue={delayValue}
actionHandler={actionHandler}
setExpanded={setExpanded}
/>
</div>
)}
</div>
)}
</Draggable>
@@ -168,4 +203,3 @@ const EventBlock = (props) => {
};
export default memo(EventBlock, areEqual);
// export default EventBlock;
@@ -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;
}
@@ -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 (
<BlockBlock index={index} data={data} eventsHandler={eventsHandler} />
<BlockBlock index={index} data={data} actionHandler={actionHandler} />
);
case 'delay':
return (
<DelayBlock index={index} data={data} eventsHandler={eventsHandler} />
<DelayBlock
index={index}
data={data}
eventsHandler={eventsHandler}
actionHandler={actionHandler}
/>
);
default:
break;