-
- {groupedCandidates.map((rowCandidates, i) => (
-
- {rowCandidates.map(time => {
- const slotProps = getCandidateSlotProps(time, duration, minHour, maxHour);
- const participants = availability?.find(a => a.startDt === `${date}T${time}`);
- return (
- !candidates.includes(time)}
- onDelete={() => handleRemoveSlot(time)}
- onChangeSlotTime={newStartTime => handleUpdateSlot(time, newStartTime)}
- text={
- participants &&
- plural(participants.availableCount, {
- 0: 'No participants registered',
- one: '# participant registered',
- other: '# participants registered',
- })
- }
- />
- );
- })}
-
- ))}
-
-
- }
- on="click"
- position="bottom center"
- onOpen={() => setTimeslotPopupOpen(true)}
- onClose={handlePopupClose}
- open={newTimeslotPopupOpen}
- onKeyDown={evt => {
- const canBeAdded = timeslotTime && !candidates.includes(timeslotTime);
- if (evt.key === 'Enter' && canBeAdded) {
- handleAddSlot(timeslotTime);
- handlePopupClose();
- }
+ return (
+
+
{
+ handleMouseDown(event);
+ handleTimelineMouseLeave();
}}
- className={styles['timepicker-popup']}
- content={
- <>
-
setTimeslotTime(time ? time.format(DEFAULT_TIME_FORMAT) : null)}
- allowEmpty={false}
- // keep the picker in the DOM tree of the surrounding element
- getPopupContainer={node => node}
- />
- {
- handleAddSlot(timeslotTime);
- handlePopupClose();
+ onMouseMove={handleTimelineMouseMove}
+ onMouseLeave={handleTimelineMouseLeave}
+ >
+
+
+ {groupedCandidates.map((rowCandidates, i) => (
+
{
+ setIsHoveringSlot(true);
+ }}
+ onMouseLeave={() => {
+ setIsHoveringSlot(false);
}}
- disabled={!timeslotTime || candidates.includes(timeslotTime)}
>
-
-
- >
- }
- />
+ {rowCandidates.map(time => {
+ const slotProps = getCandidateSlotProps(time, duration, minHour, maxHour);
+ const participants = availability?.find(a => a.startDt === `${date}T${time}`);
+ return (
+ !isTimeSlotTaken(time)}
+ onDelete={event => {
+ // Prevent the event from bubbling up to the parent div
+ event.stopPropagation();
+ handleRemoveSlot(event, time);
+ }}
+ onMouseEnter={() => {
+ setIsHoveringSlot(true);
+ }}
+ onMouseLeave={() => {
+ setIsHoveringSlot(false);
+ }}
+ onChangeSlotTime={newStartTime => handleUpdateSlot(time, newStartTime)}
+ text={
+ participants &&
+ plural(participants.availableCount, {
+ 0: 'No participants registered',
+ one: '# participant registered',
+ other: '# participants registered',
+ })
+ }
+ />
+ );
+ })}
+
+ ))}
+
+ e.stopPropagation()} className={styles['add-btn-wrapper']}>
+
e.stopPropagation()}
+ />
+ }
+ on="click"
+ onMouseMove={e => {
+ e.stopPropagation();
+ }}
+ position="bottom center"
+ onOpen={evt => {
+ // Prevent the event from bubbling up to the parent div
+ evt.stopPropagation();
+ setTimeslotPopupOpen(true);
+ }}
+ onClose={handlePopupClose}
+ open={newTimeslotPopupOpen}
+ onKeyDown={evt => {
+ const canBeAdded = timeslotTime && !isTimeSlotTaken(timeslotTime);
+ if (evt.key === 'Enter' && canBeAdded) {
+ handleAddSlot(timeslotTime);
+ handlePopupClose();
+ }
+ }}
+ className={styles['timepicker-popup']}
+ content={
+ e.stopPropagation()}
+ onMouseMove={e => {
+ e.stopPropagation();
+ }}
+ >
+ setTimeslotTime(time ? time.format(DEFAULT_TIME_FORMAT) : null)}
+ onMouseMove={e => e.stopPropagation()}
+ allowEmpty={false}
+ // keep the picker in the DOM tree of the surrounding element
+ getPopupContainer={node => node}
+ />
+ e.stopPropagation()}
+ onClick={() => {
+ handleAddSlot(timeslotTime);
+ handlePopupClose();
+ }}
+ disabled={!timeslotTime || isTimeSlotTaken(timeslotTime)}
+ >
+ e.stopPropagation()} />
+
+
+ }
+ />
+
+
- ) : (
+ );
+}
+
+TimelineInput.propTypes = {
+ minHour: PropTypes.number.isRequired,
+ maxHour: PropTypes.number.isRequired,
+};
+
+function ClickToAddTimeSlots({startEditing, copyTimeSlots}) {
+ const pastCandidates = useSelector(getPreviousDayTimeslots);
+
+ return (
-
+
Click to add time slots
{pastCandidates && (
-
+
Copy time slots from previous day
@@ -298,25 +417,52 @@ function TimelineInput({minHour, maxHour}) {
);
}
-TimelineInput.propTypes = {
- minHour: PropTypes.number.isRequired,
- maxHour: PropTypes.number.isRequired,
+ClickToAddTimeSlots.propTypes = {
+ startEditing: PropTypes.func.isRequired,
+ copyTimeSlots: PropTypes.func.isRequired,
};
function TimelineContent({busySlots: allBusySlots, minHour, maxHour}) {
+ const dispatch = useDispatch();
+ const [editing, setEditing] = useState(false);
+ const date = useSelector(getCreationCalendarActiveDate);
+ const pastCandidates = useSelector(getPreviousDayTimeslots);
+ const candidates = useSelector(getTimeslotsForActiveDate);
+
+ const copyTimeSlots = () => {
+ pastCandidates.forEach(time => {
+ dispatch(addTimeslot(date, time));
+ });
+ setEditing(true);
+ };
+
+ if (!editing && candidates.length === 0) {
+ return (
+
setEditing(true)} copyTimeSlots={copyTimeSlots} />
+ );
+ }
+
return (
-
- {allBusySlots.map(slot => (
-
- ))}
- {allBusySlots.map(({busySlots, participant}) =>
- busySlots.map(slot => {
- const key = `${participant.email}-${slot.startTime}-${slot.endTime}`;
- return
;
- })
+ <>
+
+ {allBusySlots.map(slot => (
+
+ ))}
+ {allBusySlots.map(({busySlots, participant}) =>
+ busySlots.map(slot => {
+ const key = `${participant.email}-${slot.startTime}-${slot.endTime}`;
+ return ;
+ })
+ )}
+
+
+ {editing && candidates.length === 0 && (
+
+
+ Click the timeline to add your first time slot
+
)}
-
-
+ >
);
}
@@ -352,7 +498,8 @@ export default function Timeline({date, availability, defaultMinHour, defaultMax
duration,
format,
};
- setHourSpan(getHourSpan(input));
+ const shouldSpanTwoDays = false;
+ setHourSpan(getHourSpan(input, shouldSpanTwoDays));
}, [candidates, defaultHourSpan, defaultMaxHour, defaultMinHour, duration]);
const creationTimezone = localStorage.getItem('creationTimezone');
@@ -431,7 +578,7 @@ Timeline.propTypes = {
};
Timeline.defaultProps = {
- defaultMinHour: 8,
+ defaultMinHour: 0,
defaultMaxHour: 24,
hourStep: 2,
};
diff --git a/newdle/client/src/components/creation/timeslots/Timeline.module.scss b/newdle/client/src/components/creation/timeslots/Timeline.module.scss
index 3f918332..38498635 100644
--- a/newdle/client/src/components/creation/timeslots/Timeline.module.scss
+++ b/newdle/client/src/components/creation/timeslots/Timeline.module.scss
@@ -2,17 +2,19 @@
$row-height: 50px;
$label-width: 180px;
+$rows-border-width: 5px;
.timeline {
position: relative;
margin: 4px;
- @media screen and (min-width: 1200px) {
- margin-left: $label-width;
- }
.timeline-title {
display: flex;
justify-content: space-between;
+
+ @media screen and (min-width: 1200px) {
+ margin-left: $label-width;
+ }
}
.timeline-date {
@@ -20,8 +22,8 @@ $label-width: 180px;
}
.timeline-hours {
- margin-left: 30px;
- margin-right: 10px;
+ margin-left: $rows-border-width;
+ margin-right: $rows-border-width;
color: $grey;
height: $row-height;
position: relative;
@@ -35,10 +37,6 @@ $label-width: 180px;
}
}
- .timeline-slot-picker {
- margin-left: 10px;
- margin-right: 10px;
- }
@media screen and (min-width: 990px) {
.timeline-slot-picker {
//breakpoint happens at 990, if no width specified, the view is jumpy
@@ -48,8 +46,9 @@ $label-width: 180px;
}
.timeline-rows {
position: relative;
- margin-left: 20px;
- margin-right: 10px;
+ background-color: lighten($green, 27%);
+ border: $rows-border-width solid lighten($green, 22%);
+
.timeline-row {
height: $row-height;
display: flex;
@@ -140,6 +139,7 @@ $label-width: 180px;
z-index: 1;
&.candidate {
+ box-sizing: border-box;
background-color: $green;
border: 1px solid darken($green, 4%);
height: 40px;
@@ -176,6 +176,11 @@ $label-width: 180px;
margin-left: -5px;
}
+ .add-first-text {
+ color: $grey;
+ margin-top: 10px;
+ }
+
.timeline-input {
opacity: 0.6;
display: flex;
@@ -185,7 +190,7 @@ $label-width: 180px;
border-radius: 3px;
box-sizing: content-box;
flex-basis: 100%;
- margin-left: 5px;
+ cursor: pointer;
.timeline-candidates {
display: flex;
@@ -211,10 +216,13 @@ $label-width: 180px;
}
&.edit {
- background-color: lighten($green, 27%);
- border: 5px solid lighten($green, 22%);
- margin: -5px -40px;
- padding: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+
+ .add-btn-wrapper {
+ display: flex;
+ align-items: center;
+ }
.add-btn {
margin-left: auto;
diff --git a/newdle/client/src/locales/es/messages.po b/newdle/client/src/locales/es/messages.po
index 562a9567..8480a2ca 100644
--- a/newdle/client/src/locales/es/messages.po
+++ b/newdle/client/src/locales/es/messages.po
@@ -111,7 +111,11 @@ msgstr ""
msgid "Choose your participants"
msgstr "Elige a los participantes"
-#: src/components/creation/timeslots/Timeline.js:289
+#: src/components/creation/timeslots/Timeline.js:462
+msgid "Click the timeline to add your first time slot"
+msgstr ""
+
+#: src/components/creation/timeslots/Timeline.js:408
msgid "Click to add time slots"
msgstr ""
@@ -141,7 +145,7 @@ msgstr ""
msgid "Copied!"
msgstr ""
-#: src/components/creation/timeslots/Timeline.js:294
+#: src/components/creation/timeslots/Timeline.js:413
msgid "Copy time slots from previous day"
msgstr ""
@@ -402,7 +406,7 @@ msgstr ""
msgid "Please log in again to confirm your identity"
msgstr ""
-#: src/components/creation/timeslots/Timeline.js:373
+#: src/components/creation/timeslots/Timeline.js:520
msgid "Revert to the local timezone"
msgstr ""
@@ -519,11 +523,11 @@ msgstr ""
msgid "Timeslots"
msgstr ""
-#: src/components/creation/timeslots/Timeline.js:381
+#: src/components/creation/timeslots/Timeline.js:528
msgid "Timezone"
msgstr ""
-#: src/components/creation/timeslots/Timeline.js:379
+#: src/components/creation/timeslots/Timeline.js:526
msgid "Timezone {revertIcon}"
msgstr ""
@@ -633,7 +637,7 @@ msgstr ""
msgid "switch back to the <0>{defaultUserTz}0> timezone"
msgstr ""
-#: src/components/creation/timeslots/Timeline.js:227
+#: src/components/creation/timeslots/Timeline.js:316
msgid "{0, plural, =0 {No participants registered} one {# participant registered} other {# participants registered}}"
msgstr ""
diff --git a/newdle/client/src/util/date.js b/newdle/client/src/util/date.js
index a671ee21..5b0a5a12 100644
--- a/newdle/client/src/util/date.js
+++ b/newdle/client/src/util/date.js
@@ -18,7 +18,7 @@ export function overlaps([start1, end1], [start2, end2]) {
return start1.isBefore(end2) && start2.isBefore(end1);
}
-export function getHourSpan(input) {
+export function getHourSpan(input, shouldSpanTwoDays = true) {
const {timeSlots, defaultHourSpan, defaultMinHour, defaultMaxHour, duration, format} = input;
const timeSlotsMoment = timeSlots.map(c => toMoment(c, format));
const minTimelineHour = Math.min(...timeSlotsMoment.map(timeSlot => timeSlot.hour()));
@@ -34,7 +34,7 @@ export function getHourSpan(input) {
timeSlot => !timeSlot.isSame(timeSlot.clone().add(duration, 'm'), 'day')
) !== undefined;
let maxTimelineHour;
- if (spansOverTwoDays) {
+ if (spansOverTwoDays && shouldSpanTwoDays) {
maxTimelineHour = 24 + maxTimeline.hour();
} else {
maxTimelineHour = maxTimeline.minutes() ? maxTimeline.hour() + 1 : maxTimeline.hour();