Add disabled day display for large timezone differences. Disabled days will appear if a user is in a timezone so distant, that their availability pops into the next or previous day.

This commit is contained in:
Miroslav Vasilev 2024-01-13 00:07:50 +02:00
parent 4abf196738
commit 7032156177
8 changed files with 124 additions and 62 deletions

View file

@ -52,7 +52,7 @@ services:
In the end, your folder structure should be as follows: In the end, your folder structure should be as follows:
``` ```
installationDir/ installationDir/
| |-frontend/ |-frontend/
| |-dist/ | |-dist/
|-findtheti-me |-findtheti-me
``` ```

View file

@ -60,8 +60,6 @@ const AvailabilityPicker = (props: {
let existingTime = day.availableTimes.findIndex(t => utils.dayjsIsBetweenUnixExclusive(t.fromTime, time, t.toTime)); let existingTime = day.availableTimes.findIndex(t => utils.dayjsIsBetweenUnixExclusive(t.fromTime, time, t.toTime));
if (existingTime >= 0) { if (existingTime >= 0) {
console.log(`delete ${existingTime} from`, day)
day.availableTimes.splice(existingTime, 1); day.availableTimes.splice(existingTime, 1);
let dayIndex = props.days.findIndex(d => d.forDate.unix() === day.forDate.unix()); let dayIndex = props.days.findIndex(d => d.forDate.unix() === day.forDate.unix());
@ -178,7 +176,6 @@ const AvailabilityPicker = (props: {
key={day.forDate.unix()} key={day.forDate.unix()}
day={day} day={day}
eventType={props.eventType} eventType={props.eventType}
currentTotalRespondents={props.availabilityHeatmap.maxNumberOfRespondents}
halfHourDisplayHeight={HALFHOUR_DISPLAY_HEIGHT} halfHourDisplayHeight={HALFHOUR_DISPLAY_HEIGHT}
availabilityHeatmap={props.availabilityHeatmap} availabilityHeatmap={props.availabilityHeatmap}
onMouseEnterHalfhour={(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => { onMouseEnterHalfhour={(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => {

View file

@ -1,6 +1,6 @@
import { AvailabilityDay, UserAvailabilityHeatmap } from "../types/Availabilities"; import { AvailabilityDay, UserAvailabilityHeatmap } from "../types/Availabilities";
import utils from "../utils"; import utils from "../utils";
import { Box, Card, Divider, Typography } from "@mui/material"; import { Box, Card, Divider, Tooltip, Typography } from "@mui/material";
import { EventTypes } from "../types/Event"; import { EventTypes } from "../types/Event";
import AvailabilityPickerHour from "./AvailabilityPickerHour"; import AvailabilityPickerHour from "./AvailabilityPickerHour";
import "./css/AvailabilityPicker.css"; import "./css/AvailabilityPicker.css";
@ -9,6 +9,7 @@ import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { InfoOutlined } from "@mui/icons-material";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -18,7 +19,6 @@ const AvailabilityPickerDay = (props: {
day: AvailabilityDay, day: AvailabilityDay,
eventType: String, eventType: String,
halfHourDisplayHeight: number, halfHourDisplayHeight: number,
currentTotalRespondents: number,
availabilityHeatmap: UserAvailabilityHeatmap, availabilityHeatmap: UserAvailabilityHeatmap,
onMouseEnterHalfhour: (e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => void, onMouseEnterHalfhour: (e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => void,
onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs, isDelete: boolean) => void onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs, isDelete: boolean) => void
@ -28,23 +28,32 @@ const AvailabilityPickerDay = (props: {
let hours: JSX.Element[] = []; let hours: JSX.Element[] = [];
for (var i = 0; i < 24; i++) { for (var i = 0; i < 24; i++) {
let fullHourTime = props.day.forDate.set("hour", i); let fullHourTime = props.day.forDate.hour(i);
let halfHourTime = fullHourTime.add(30, "minutes"); let halfHourTime = utils.createHalfHourFromFullHour(fullHourTime);
hours.push( hours.push(
<AvailabilityPickerHour <AvailabilityPickerHour
key={fullHourTime.unix()} key={fullHourTime.unix()}
disabled={props.day.disabled}
dateTime={fullHourTime} dateTime={fullHourTime}
halfHourDisplayHeight={props.halfHourDisplayHeight} halfHourDisplayHeight={props.halfHourDisplayHeight}
currentTotalRespondents={props.currentTotalRespondents} currentTotalRespondents={props.availabilityHeatmap.maxNumberOfRespondents}
namesMarkedFullHourAsAvailable={props.availabilityHeatmap.getNamesAt(fullHourTime.unix())} namesMarkedFullHourAsAvailable={props.availabilityHeatmap.getNamesAt(fullHourTime.unix())}
namesMarkedHalfHourAsAvailable={props.availabilityHeatmap.getNamesAt(fullHourTime.add(30, "minutes").unix())} namesMarkedHalfHourAsAvailable={props.availabilityHeatmap.getNamesAt(halfHourTime.unix())}
isFullHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, fullHourTime, a.toTime))} isFullHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, fullHourTime, a.toTime))}
isHalfHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, halfHourTime, a.toTime))} isHalfHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, halfHourTime, a.toTime))}
onMouseEnterHalfhour={(e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs): void => { onMouseEnterHalfhour={(e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs): void => {
if (props.day.disabled) {
return;
}
props.onMouseEnterHalfhour(e, time); props.onMouseEnterHalfhour(e, time);
}} }}
onMouseClickOnHalfhour={(time: dayjs.Dayjs, isDelete: boolean): void => { onMouseClickOnHalfhour={(time: dayjs.Dayjs, isDelete: boolean): void => {
if (props.day.disabled) {
return;
}
props.onMouseClickHalfhour(props.day, time, isDelete); props.onMouseClickHalfhour(props.day, time, isDelete);
}} }}
/> />
@ -64,25 +73,29 @@ const AvailabilityPickerDay = (props: {
<Box <Box
sx={{ width: "100%" }} sx={{ width: "100%" }}
padding={1} padding={1}
height={"50px"}
> >
{
(props.eventType === EventTypes.WEEK) &&
<Typography> <Typography>
{ props.day.forDate.format("dddd") } {
</Typography> (props.eventType === EventTypes.WEEK) && props.day.forDate.format("dddd")
} }
{ {
(props.eventType === EventTypes.DAY) && (props.eventType === EventTypes.DAY) && "Any Day"
<Typography>
Any Day
</Typography>
} }
{ {
(props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && props.day.forDate.format("LL")
<Typography>
{ props.day.forDate.format("LL") }
</Typography>
} }
{
props.day.disabled &&
<Tooltip
title={props.day.disabled ? "This day is disabled and only shown as a result of timezone differences between yourself and another respondent" : ""}
placement="top"
arrow
>
<InfoOutlined sx={{ ml: 1 }} fontSize="inherit" />
</Tooltip>
}
</Typography>
</Box> </Box>
<Divider></Divider> <Divider></Divider>

View file

@ -14,6 +14,7 @@ dayjs.extend(localizedFormat)
const AvailabilityPickerHour = (props: { const AvailabilityPickerHour = (props: {
dateTime: Dayjs, dateTime: Dayjs,
disabled: boolean,
isFullHourSelected: boolean, isFullHourSelected: boolean,
isHalfHourSelected: boolean, isHalfHourSelected: boolean,
halfHourDisplayHeight: number, halfHourDisplayHeight: number,
@ -28,12 +29,16 @@ const AvailabilityPickerHour = (props: {
} }
const heatMapColorforValue = (value: number) => { const heatMapColorforValue = (value: number) => {
if (value === 0 || props.currentTotalRespondents === 0) { // if (value === 0 || props.currentTotalRespondents === 0) {
return 'inherit'; // return 'inherit';
} // }
// if (value === 1 && props.currentTotalRespondents === 1) {
// return "hsl(" + 0 + ", 75%, 35%) !important";
// }
var h = (1.0 - (value / props.currentTotalRespondents)) * 240 var h = (1.0 - (value / props.currentTotalRespondents)) * 240
return "hsl(" + h + ", 75%, 35%)"; return "hsl(" + h + ", 75%, 35%) !important";
} }
return ( return (
@ -51,8 +56,10 @@ const AvailabilityPickerHour = (props: {
enterDelay={500} enterDelay={500}
> >
<Box <Box
className={classNames("full-hour", { "selected-availability": props.isFullHourSelected })} sx={{
bgcolor={heatMapColorforValue(props.namesMarkedFullHourAsAvailable.length)} bgcolor: props.namesMarkedFullHourAsAvailable.length > 0 ? heatMapColorforValue(props.namesMarkedFullHourAsAvailable.length) : 'inherit'
}}
className={classNames("full-hour", { "selected-availability": props.isFullHourSelected, "hour-disabled": props.disabled })}
height={props.halfHourDisplayHeight} height={props.halfHourDisplayHeight}
onMouseEnter={(e) => props.onMouseEnterHalfhour(e, props.dateTime)} onMouseEnter={(e) => props.onMouseEnterHalfhour(e, props.dateTime)}
onMouseDown={(e) => { onMouseDown={(e) => {
@ -78,16 +85,18 @@ const AvailabilityPickerHour = (props: {
enterDelay={500} enterDelay={500}
> >
<Box <Box
className={classNames("half-hour", { "selected-availability": props.isHalfHourSelected })} sx={{
bgcolor={heatMapColorforValue(props.namesMarkedHalfHourAsAvailable.length)} bgcolor: props.namesMarkedHalfHourAsAvailable.length > 0 ? heatMapColorforValue(props.namesMarkedHalfHourAsAvailable.length) : 'inherit'
}}
className={classNames("half-hour", { "selected-availability": props.isHalfHourSelected, "hour-disabled": props.disabled })}
height={props.halfHourDisplayHeight} height={props.halfHourDisplayHeight}
onMouseEnter={(e) => props.onMouseEnterHalfhour(e, props.dateTime.add(30, "minutes"))} onMouseEnter={(e) => props.onMouseEnterHalfhour(e, utils.createHalfHourFromFullHour(props.dateTime))}
onMouseDown={(e) => { onMouseDown={(e) => {
if (e.button !== 0 && e.button !== 2) { if (e.button !== 0 && e.button !== 2) {
return; return;
} }
props.onMouseClickOnHalfhour(props.dateTime.add(30, "minutes"), e.button === 2); props.onMouseClickOnHalfhour(utils.createHalfHourFromFullHour(props.dateTime), e.button === 2);
}} }}
/> />
</DisableableTooltip> </DisableableTooltip>

View file

@ -22,7 +22,7 @@ div.hour-light {
background-color: var(--hour-light-color); background-color: var(--hour-light-color);
} }
div.hour-dark { div.hour-disabled {
width: 100%; width: 100%;
border-top: solid 1px; border-top: solid 1px;
border-color: var(--hour-border-bolor); border-color: var(--hour-border-bolor);
@ -47,7 +47,7 @@ div.selected-availability {
background-color: var(--hover-color); background-color: var(--hover-color);
} */ } */
div.full-hour:active, div.half-hour:active { div.full-hour:not(.hour-disabled):active, div.half-hour:not(.hour-disabled):active {
background-color: var(--active-color); background-color: var(--active-color);
} }

View file

@ -33,8 +33,6 @@ export default function ExistingEventPage() {
useEffect(() => { useEffect(() => {
utils.showSpinner(); utils.showSpinner();
let localTimezone = dayjs.tz.guess();
Promise.all([ Promise.all([
utils.performRequest(`/api/events/${eventId}`) utils.performRequest(`/api/events/${eventId}`)
.then(result => setEvent({ .then(result => setEvent({
@ -52,6 +50,8 @@ export default function ExistingEventPage() {
.then((result: [{ id: number, from_date: string, to_date: string, user_name: string }]) => { .then((result: [{ id: number, from_date: string, to_date: string, user_name: string }]) => {
let heatmap = new UserAvailabilityHeatmap(); let heatmap = new UserAvailabilityHeatmap();
let localTimezone = dayjs.tz.guess();
const LENGTH_OF_30_MINUTES_IN_SECONDS = 1800; const LENGTH_OF_30_MINUTES_IN_SECONDS = 1800;
for (const availability of result) { for (const availability of result) {
@ -61,7 +61,30 @@ export default function ExistingEventPage() {
let startUnix = start.unix(); let startUnix = start.unix();
let endUnix = end.unix(); let endUnix = end.unix();
for (var timeInUnix = startUnix; timeInUnix <= endUnix; timeInUnix += LENGTH_OF_30_MINUTES_IN_SECONDS) { let startDay = start.startOf("day");
let endDay = end.startOf("day");
// Add day on which this availability period is starts on ( timezone difference could be so large that it's in the previous day )
// This day will be disabled and unavailable for adding own availability, unless it falls within the events own from-to range.
if (!heatmap.daysWhenAvailabilitiesPresent.some(d => d.forDate.unix() === startDay.unix())) {
heatmap.daysWhenAvailabilitiesPresent.push({
forDate: startDay,
disabled: true,
availableTimes: []
});
}
// Add day on which this availability period is set to end ( timezone difference could be so large that it flips to the next day )
// This day will be disabled and unavailable for adding own availability, unless it falls within the events own from-to range.
if (end.unix() !== endDay.unix() && !heatmap.daysWhenAvailabilitiesPresent.some(d => d.forDate.unix() === endDay.unix())) {
heatmap.daysWhenAvailabilitiesPresent.push({
forDate: endDay,
disabled: true,
availableTimes: []
});
}
for (var timeInUnix = startUnix; timeInUnix < endUnix; timeInUnix += LENGTH_OF_30_MINUTES_IN_SECONDS) {
heatmap.addName(timeInUnix, availability.user_name); heatmap.addName(timeInUnix, availability.user_name);
} }
} }
@ -70,7 +93,7 @@ export default function ExistingEventPage() {
}) })
.catch(e => toast.error(e)) .catch(e => toast.error(e))
]) ])
.finally(() => utils.hideSpinner());; .finally(() => utils.hideSpinner())
}, [eventId]); }, [eventId]);
@ -106,25 +129,41 @@ export default function ExistingEventPage() {
let localFromDate = event.fromDate.tz(localTimezone); let localFromDate = event.fromDate.tz(localTimezone);
let localToDate = event.toDate.tz(localTimezone); let localToDate = event.toDate.tz(localTimezone);
var days: AvailabilityDay[] = [];
switch (event.eventType) { switch (event.eventType) {
case EventTypes.SPECIFIC_DATE: { case EventTypes.SPECIFIC_DATE: {
createAvailabilitiesBasedOnInitialDate(localFromDate, 1); days = createAvailabilitiesBasedOnInitialDate(localFromDate, 1);
break; break;
} }
case EventTypes.DATE_RANGE: { case EventTypes.DATE_RANGE: {
createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false))); days = createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false)));
break; break;
} }
case EventTypes.DAY: { case EventTypes.DAY: {
createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone); days = createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone);
break; break;
} }
case EventTypes.WEEK: { case EventTypes.WEEK: {
createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone); days = createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone);
break; break;
} }
} }
}, [event]);
availabilityHeatmap?.daysWhenAvailabilitiesPresent.forEach(hd => {
let createdDay = days.find(cd => cd.forDate.unix() === hd.forDate.unix());
if (createdDay) {
createdDay.disabled = false;
} else {
days.push(hd);
}
});
days.sort((a, b) => a.forDate.unix() - b.forDate.unix());
setDays([...days])
}, [event, availabilityHeatmap]);
useEffect(() => { useEffect(() => {
var valid = !utils.isNullOrUndefined(userName) && userName !== ""; var valid = !utils.isNullOrUndefined(userName) && userName !== "";
@ -134,23 +173,24 @@ export default function ExistingEventPage() {
setCanSubmit(valid); setCanSubmit(valid);
}, [userName, days]); }, [userName, days]);
const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => { const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string): AvailabilityDay[] => {
createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays); return createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
} }
const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number) => { const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number): AvailabilityDay[] => {
let availabilities: AvailabilityDay[] = []; let availabilities: AvailabilityDay[] = [];
for (var i: number = 0; i < numberOfDays; i++) { for (var i: number = 0; i < numberOfDays; i++) {
let availability: AvailabilityDay = { let availability: AvailabilityDay = {
forDate: date.add(i, "day").startOf("day"), forDate: date.add(i, "day").startOf("day"),
disabled: false,
availableTimes: [] availableTimes: []
} }
availabilities.push(availability); availabilities.push(availability);
} }
setDays(availabilities); return availabilities;
} }
const submitAvailabilities = () => { const submitAvailabilities = () => {

View file

@ -7,6 +7,7 @@ export type AvailabilityTime = {
export type AvailabilityDay = { export type AvailabilityDay = {
forDate: Dayjs, forDate: Dayjs,
disabled: boolean,
availableTimes: AvailabilityTime[] availableTimes: AvailabilityTime[]
} }
@ -15,12 +16,11 @@ export type UserAvailabilityHeatmapValue = {
} }
export class UserAvailabilityHeatmap { export class UserAvailabilityHeatmap {
private map: UserAvailabilityHeatmapValue[]; private map: any = {};
public maxNumberOfRespondents: number; public maxNumberOfRespondents: number = 0;
public daysWhenAvailabilitiesPresent: AvailabilityDay[] = [];
constructor() { constructor() {
this.map = [];
this.maxNumberOfRespondents = 0;
} }
addName(unixTime: number, name: String): void { addName(unixTime: number, name: String): void {

View file

@ -78,6 +78,9 @@ const utils = {
}, },
isNullOrUndefined: (thing: any): boolean => { isNullOrUndefined: (thing: any): boolean => {
return thing === null || thing === undefined; return thing === null || thing === undefined;
},
createHalfHourFromFullHour: (fullHour: Dayjs): Dayjs => {
return fullHour.add(30, "minutes");
} }
} }