import { Box, Card, Divider, Stack, Theme, Typography, useTheme } from "@mui/material"; import dayjs, { Dayjs } from "dayjs"; import { MouseEvent, ReactNode, useEffect, useState } from "react"; import * as utc from 'dayjs/plugin/utc'; import * as timezone from 'dayjs/plugin/timezone'; import * as localizedFormat from 'dayjs/plugin/localizedFormat'; import utils from "../utils"; import { EventTypes } from "../types/Event"; import "./css/AvailabilityPicker.css"; // import { alpha } from '@material-ui/core/styles/colorManipulator'; dayjs.extend(utc) dayjs.extend(timezone) dayjs.extend(localizedFormat) type AvailabilityTime = { fromTime: Dayjs, toTime: Dayjs } type AvailabilityDay = { forDate: Dayjs, availableTimes: AvailabilityTime[] } type GhostPreviewProps = { top: number, left: number, width: String, height: String } const HALFHOUR_DISPLAY_HEIGHT: number = 15; const DAY_DISPLAY_WIDTH: String = "150px"; export default function AvailabilityPicker(props: { fromDate: Dayjs, toDate: Dayjs, eventType: String, availabilityDurationInMinutes: number }) { const theme: Theme = useTheme(); const [days, setDays] = useState([]); const [ghostPreviewProps, setGhostPreviewProps] = useState(); useEffect(() => { let localTimezone = dayjs.tz.guess(); let localFromDate = props.fromDate.tz(localTimezone); let localToDate = props.toDate.tz(localTimezone); switch (props.eventType) { case EventTypes.SPECIFIC_DATE: { createAvailabilitiesBasedOnInitialDate(localFromDate, 1); break; } case EventTypes.DATE_RANGE: { createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false))); break; } case EventTypes.DAY: { createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone); break; } case EventTypes.WEEK: { createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone); break; } } }, [props]); function createAvailabilitiesBasedOnUnspecifiedInitialDate(numberOfDays: number, tz: string) { createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays); } function createAvailabilitiesBasedOnInitialDate(date: Dayjs, numberOfDays: number) { let availabilities: AvailabilityDay[] = []; for (var i: number = 0; i < numberOfDays; i++) { let availability: AvailabilityDay = { forDate: date.add(i, "day").startOf("day"), availableTimes: [] } availabilities.push(availability); } setDays(availabilities); } function displayGhostPeriod(e: React.MouseEvent, time: Dayjs) { let timeInMinutes = (time.hour() * 60) + time.minute(); let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24 * 60; if (timeLeftInDayLessThanDuration) { return; } let scrollTop = document.getElementById('availability-picker')?.scrollTop ?? 0; let scrollLeft = document.getElementById('availability-picker')?.scrollLeft ?? 0; const element = e.target.getBoundingClientRect(); setGhostPreviewProps({ top: e.target?.offsetTop - scrollTop, left: e.target?.offsetLeft - scrollLeft, width: element.width, height: `${(props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT}px` }) } function createAvailability(day: AvailabilityDay, time: Dayjs) { let fromTime = time; let toTime = time.add(props.availabilityDurationInMinutes, "minutes"); let existingTimeContainingFrom = day.availableTimes.findIndex(t => (t.fromTime.isBefore(fromTime) || t.fromTime.isSame(fromTime)) && (t.toTime.isAfter(fromTime) || t.toTime.isSame(fromTime))); let existingTimeContainingTo = day.availableTimes.findIndex(t => (t.fromTime.isBefore(toTime) || t.fromTime.isSame(toTime)) && (t.toTime.isAfter(toTime) || t.toTime.isSame(toTime))); // the newly created availability crosses another single one. Both have the same from and to. Do nothing. if (existingTimeContainingFrom >= 0 && existingTimeContainingTo >= 0 && existingTimeContainingFrom === existingTimeContainingTo) { return; } // the newly created availability crosses 2 existing ones. Combine all of them into a single one. if (existingTimeContainingFrom >= 0 && existingTimeContainingTo >= 0 && existingTimeContainingFrom !== existingTimeContainingTo) { let newFrom = day.availableTimes[existingTimeContainingFrom].fromTime; let newTo = day.availableTimes[existingTimeContainingFrom].toTime; day.availableTimes.splice(existingTimeContainingFrom); day.availableTimes.splice(existingTimeContainingTo); day.availableTimes.push({ fromTime: newFrom, toTime: newTo }); return; } // The newly created availability from is within an existing one. Combine the 2 into one. if (existingTimeContainingFrom >= 0 && existingTimeContainingTo < 0) { let newFrom = day.availableTimes[existingTimeContainingFrom].fromTime; day.availableTimes.splice(existingTimeContainingFrom); day.availableTimes.push({ fromTime: newFrom, toTime: toTime }); return; } // The newly created availability to is within an existing one. Combine the 2 into one. if (existingTimeContainingFrom >= 0 && existingTimeContainingTo < 0) { let newTo = day.availableTimes[existingTimeContainingFrom].toTime; day.availableTimes.splice(existingTimeContainingFrom); day.availableTimes.push({ fromTime: fromTime, toTime: newTo }); return; } day.availableTimes.push({ fromTime: fromTime, toTime: toTime }); setDays([...days]) } function isSelectedAvailability(day: AvailabilityDay, time: Dayjs): boolean { return day.availableTimes.some(t => (t.fromTime.isBefore(time) || t.fromTime.isSame(time)) && (t.toTime.isAfter(time) || t.toTime.isSame(time))); } function generateDay(day: AvailabilityDay) { const HOUR_TEXT_COLOR: String = "#ddd"; let hours = []; for (var i = 0; i < 24; i++) { let time = day.forDate.set("hour", i).set("minute", 0).set("second", 0); hours.push( ) => displayGhostPeriod(e, time)} onClick={(e) => createAvailability(day, time)} > { utils.formatTimeFromHourOfDay(i, 0) } ) => displayGhostPeriod(e, time.add(30, "minutes"))} onClick={(e) => createAvailability(day, time.add(30, "minutes"))} > ); } return ( { (props.eventType === EventTypes.WEEK) && { day.forDate.format("dddd") } } { (props.eventType === EventTypes.DAY) && Any Day } { (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && { day.forDate.format("LL") } } {hours} ); } return ( setGhostPreviewProps(null)} > { days.map(a => generateDay(a)) } { (ghostPreviewProps !== null) && } ); }