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,8 +52,8 @@ services:
In the end, your folder structure should be as follows:
```
installationDir/
| |-frontend/
| |-dist/
|-frontend/
| |-dist/
|-findtheti-me
```
5. Next, create a `.env` file in the root of the installation directory, and look at `.env.example` for what should be in there

View file

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

View file

@ -1,6 +1,6 @@
import { AvailabilityDay, UserAvailabilityHeatmap } from "../types/Availabilities";
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 AvailabilityPickerHour from "./AvailabilityPickerHour";
import "./css/AvailabilityPicker.css";
@ -9,6 +9,7 @@ import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import React, { useMemo } from "react";
import { InfoOutlined } from "@mui/icons-material";
dayjs.extend(utc)
dayjs.extend(timezone)
@ -18,7 +19,6 @@ const AvailabilityPickerDay = (props: {
day: AvailabilityDay,
eventType: String,
halfHourDisplayHeight: number,
currentTotalRespondents: number,
availabilityHeatmap: UserAvailabilityHeatmap,
onMouseEnterHalfhour: (e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => void,
onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs, isDelete: boolean) => void
@ -28,23 +28,32 @@ const AvailabilityPickerDay = (props: {
let hours: JSX.Element[] = [];
for (var i = 0; i < 24; i++) {
let fullHourTime = props.day.forDate.set("hour", i);
let halfHourTime = fullHourTime.add(30, "minutes");
let fullHourTime = props.day.forDate.hour(i);
let halfHourTime = utils.createHalfHourFromFullHour(fullHourTime);
hours.push(
<AvailabilityPickerHour
key={fullHourTime.unix()}
disabled={props.day.disabled}
dateTime={fullHourTime}
halfHourDisplayHeight={props.halfHourDisplayHeight}
currentTotalRespondents={props.currentTotalRespondents}
currentTotalRespondents={props.availabilityHeatmap.maxNumberOfRespondents}
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))}
isHalfHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, halfHourTime, a.toTime))}
onMouseEnterHalfhour={(e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs): void => {
if (props.day.disabled) {
return;
}
props.onMouseEnterHalfhour(e, time);
}}
onMouseClickOnHalfhour={(time: dayjs.Dayjs, isDelete: boolean): void => {
if (props.day.disabled) {
return;
}
props.onMouseClickHalfhour(props.day, time, isDelete);
}}
/>
@ -61,29 +70,33 @@ const AvailabilityPickerDay = (props: {
className={"day-card"}
variant="outlined"
>
<Box
sx={{ width: "100%" }}
padding={1}
>
{
(props.eventType === EventTypes.WEEK) &&
<Box
sx={{ width: "100%" }}
padding={1}
height={"50px"}
>
<Typography>
{ props.day.forDate.format("dddd") }
{
(props.eventType === EventTypes.WEEK) && props.day.forDate.format("dddd")
}
{
(props.eventType === EventTypes.DAY) && "Any Day"
}
{
(props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && props.day.forDate.format("LL")
}
{
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>
}
{
(props.eventType === EventTypes.DAY) &&
<Typography>
Any Day
</Typography>
}
{
(props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) &&
<Typography>
{ props.day.forDate.format("LL") }
</Typography>
}
</Box>
</Box>
<Divider></Divider>

View file

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

View file

@ -22,7 +22,7 @@ div.hour-light {
background-color: var(--hour-light-color);
}
div.hour-dark {
div.hour-disabled {
width: 100%;
border-top: solid 1px;
border-color: var(--hour-border-bolor);
@ -47,7 +47,7 @@ div.selected-availability {
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);
}

View file

@ -33,8 +33,6 @@ export default function ExistingEventPage() {
useEffect(() => {
utils.showSpinner();
let localTimezone = dayjs.tz.guess();
Promise.all([
utils.performRequest(`/api/events/${eventId}`)
.then(result => setEvent({
@ -52,6 +50,8 @@ export default function ExistingEventPage() {
.then((result: [{ id: number, from_date: string, to_date: string, user_name: string }]) => {
let heatmap = new UserAvailabilityHeatmap();
let localTimezone = dayjs.tz.guess();
const LENGTH_OF_30_MINUTES_IN_SECONDS = 1800;
for (const availability of result) {
@ -61,7 +61,30 @@ export default function ExistingEventPage() {
let startUnix = start.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);
}
}
@ -70,7 +93,7 @@ export default function ExistingEventPage() {
})
.catch(e => toast.error(e))
])
.finally(() => utils.hideSpinner());;
.finally(() => utils.hideSpinner())
}, [eventId]);
@ -106,25 +129,41 @@ export default function ExistingEventPage() {
let localFromDate = event.fromDate.tz(localTimezone);
let localToDate = event.toDate.tz(localTimezone);
var days: AvailabilityDay[] = [];
switch (event.eventType) {
case EventTypes.SPECIFIC_DATE: {
createAvailabilitiesBasedOnInitialDate(localFromDate, 1);
days = createAvailabilitiesBasedOnInitialDate(localFromDate, 1);
break;
}
case EventTypes.DATE_RANGE: {
createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false)));
days = createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false)));
break;
}
case EventTypes.DAY: {
createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone);
days = createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone);
break;
}
case EventTypes.WEEK: {
createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone);
days = createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone);
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(() => {
var valid = !utils.isNullOrUndefined(userName) && userName !== "";
@ -134,23 +173,24 @@ export default function ExistingEventPage() {
setCanSubmit(valid);
}, [userName, days]);
const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => {
createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string): AvailabilityDay[] => {
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[] = [];
for (var i: number = 0; i < numberOfDays; i++) {
let availability: AvailabilityDay = {
forDate: date.add(i, "day").startOf("day"),
disabled: false,
availableTimes: []
}
availabilities.push(availability);
}
setDays(availabilities);
return availabilities;
}
const submitAvailabilities = () => {

View file

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

View file

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