diff --git a/README.md b/README.md index 4a9437b..4ccf2c1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/frontend/src/components/AvailabilityPicker.tsx b/frontend/src/components/AvailabilityPicker.tsx index 7732392..46c4780 100644 --- a/frontend/src/components/AvailabilityPicker.tsx +++ b/frontend/src/components/AvailabilityPicker.tsx @@ -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, time: dayjs.Dayjs) => { diff --git a/frontend/src/components/AvailabilityPickerDay.tsx b/frontend/src/components/AvailabilityPickerDay.tsx index 4e6e2bc..18a1db4 100644 --- a/frontend/src/components/AvailabilityPickerDay.tsx +++ b/frontend/src/components/AvailabilityPickerDay.tsx @@ -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, 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( utils.dayjsIsBetweenUnixExclusive(a.fromTime, fullHourTime, a.toTime))} isHalfHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, halfHourTime, a.toTime))} onMouseEnterHalfhour={(e: React.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" > - - { - (props.eventType === EventTypes.WEEK) && + - { 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 && + + + + } - } - { - (props.eventType === EventTypes.DAY) && - - Any Day - - } - { - (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && - - { props.day.forDate.format("LL") } - - } - + diff --git a/frontend/src/components/AvailabilityPickerHour.tsx b/frontend/src/components/AvailabilityPickerHour.tsx index 37087a8..ce0a3ca 100644 --- a/frontend/src/components/AvailabilityPickerHour.tsx +++ b/frontend/src/components/AvailabilityPickerHour.tsx @@ -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} > 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} > 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); }} /> diff --git a/frontend/src/components/css/AvailabilityPicker.css b/frontend/src/components/css/AvailabilityPicker.css index 4c219fe..2a4387e 100644 --- a/frontend/src/components/css/AvailabilityPicker.css +++ b/frontend/src/components/css/AvailabilityPicker.css @@ -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); } diff --git a/frontend/src/pages/ExistingEventPage.tsx b/frontend/src/pages/ExistingEventPage.tsx index 07be298..87f9803 100644 --- a/frontend/src/pages/ExistingEventPage.tsx +++ b/frontend/src/pages/ExistingEventPage.tsx @@ -32,8 +32,6 @@ export default function ExistingEventPage() { useEffect(() => { utils.showSpinner(); - - let localTimezone = dayjs.tz.guess(); Promise.all([ utils.performRequest(`/api/events/${eventId}`) @@ -47,11 +45,13 @@ export default function ExistingEventPage() { duration: result?.duration })) .catch(e => toast.error(e)), - + utils.performRequest(`/api/events/${eventId}/availabilities`) .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 = () => { diff --git a/frontend/src/types/Availabilities.tsx b/frontend/src/types/Availabilities.tsx index f012c89..5785d04 100644 --- a/frontend/src/types/Availabilities.tsx +++ b/frontend/src/types/Availabilities.tsx @@ -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 { diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index a1fe5bb..fc311b5 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -78,6 +78,9 @@ const utils = { }, isNullOrUndefined: (thing: any): boolean => { return thing === null || thing === undefined; + }, + createHalfHourFromFullHour: (fullHour: Dayjs): Dayjs => { + return fullHour.add(30, "minutes"); } }