From 988431d0041963a2851a1ed4e5e547786721af78 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Wed, 10 Jan 2024 10:19:54 +0200 Subject: [PATCH] Optimize AvailabilityPicker performance --- frontend/package.json | 3 + .../src/components/AvailabilityPicker.tsx | 286 +++++++++--------- .../src/components/css/AvailabilityPicker.css | 37 +++ frontend/yarn.lock | 12 +- 4 files changed, 196 insertions(+), 142 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2a2ea8e..6c1b124 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,10 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.3", + "@mui/system": "^5.15.3", "@mui/x-date-pickers": "^6.18.7", + "classname": "^0.0.0", + "classnames": "^2.5.1", "dayjs": "^1.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/components/AvailabilityPicker.tsx b/frontend/src/components/AvailabilityPicker.tsx index 86fa02b..bf9b3f6 100644 --- a/frontend/src/components/AvailabilityPicker.tsx +++ b/frontend/src/components/AvailabilityPicker.tsx @@ -1,12 +1,13 @@ -import { Box, Card, Divider, Stack, Theme, Typography, useTheme } from "@mui/material"; +import { Box, Card, Divider, Stack, Typography } from "@mui/material"; import dayjs, { Dayjs } from "dayjs"; -import { MouseEvent, ReactNode, useEffect, useState } from "react"; +import { MouseEvent, 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 classNames from 'classnames'; // import { alpha } from '@material-ui/core/styles/colorManipulator'; dayjs.extend(utc) @@ -26,21 +27,125 @@ type AvailabilityDay = { type GhostPreviewProps = { top: number, left: number, - width: String, - height: String + width: number, + height: number } const HALFHOUR_DISPLAY_HEIGHT: number = 15; -const DAY_DISPLAY_WIDTH: String = "150px"; +const Hour = (props: { + dateTime: Dayjs, + isFullHourSelected: boolean, + isHalfHourSelected: boolean, + onMouseEnterHalfhour: (e: MouseEvent, time: Dayjs) => void, + onMouseClickOnHalfhour: (time: Dayjs) => void +}) => { + let isEvenHour = props.dateTime.hour() % 2 == 0; -export default function AvailabilityPicker(props: { + return ( + + props.onMouseEnterHalfhour(e, props.dateTime)} + onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime)} + > + + { utils.formatTimeFromHourOfDay(props.dateTime.hour(), 0) } + + + props.onMouseEnterHalfhour(e, props.dateTime.add(30, "minutes"))} + onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime.add(30, "minutes"))} + /> + + ); +} + +const isSelectedAvailability = (day: AvailabilityDay, time: Dayjs): boolean => { + return day.availableTimes.some(t => t.fromTime.unix() <= time.unix() && time.unix() <= t.toTime.unix()); +} + +const Day = (props: { + day: AvailabilityDay, + eventType: String, + onMouseEnterHalfhour: (e: MouseEvent, time: dayjs.Dayjs) => void, + onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs) => void +}) => { + + const generateHours = (): JSX.Element[] => { + let hours: JSX.Element[] = []; + + for (var i = 0; i < 24; i++) { + let time = props.day.forDate.set("hour", i); + hours.push( + , time: dayjs.Dayjs): void => { + props.onMouseEnterHalfhour(e, time); + }} + onMouseClickOnHalfhour={(time: dayjs.Dayjs): void => { + props.onMouseClickHalfhour(props.day, time); + }} + /> + ); + } + + return hours; + } + + return ( + + + { + (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") } + + } + + + + + {generateHours()} + + + ); +} + +const AvailabilityPicker = (props: { fromDate: Dayjs, toDate: Dayjs, eventType: String, availabilityDurationInMinutes: number -}) { - const theme: Theme = useTheme(); +}) => { const [days, setDays] = useState([]); const [ghostPreviewProps, setGhostPreviewProps] = useState(); @@ -71,11 +176,11 @@ export default function AvailabilityPicker(props: { } }, [props]); - function createAvailabilitiesBasedOnUnspecifiedInitialDate(numberOfDays: number, tz: string) { + const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => { createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays); } - function createAvailabilitiesBasedOnInitialDate(date: Dayjs, numberOfDays: number) { + const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number) => { let availabilities: AvailabilityDay[] = []; for (var i: number = 0; i < numberOfDays; i++) { @@ -90,7 +195,7 @@ export default function AvailabilityPicker(props: { setDays(availabilities); } - function displayGhostPeriod(e: React.MouseEvent, time: Dayjs) { + const displayGhostPeriod = (e: MouseEvent, time: Dayjs) => { let timeInMinutes = (time.hour() * 60) + time.minute(); let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24 * 60; @@ -101,17 +206,20 @@ export default function AvailabilityPicker(props: { let scrollTop = document.getElementById('availability-picker')?.scrollTop ?? 0; let scrollLeft = document.getElementById('availability-picker')?.scrollLeft ?? 0; + // @ts-ignore const element = e.target.getBoundingClientRect(); setGhostPreviewProps({ + // @ts-ignore top: e.target?.offsetTop - scrollTop, + // @ts-ignore left: e.target?.offsetLeft - scrollLeft, width: element.width, - height: `${(props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT}px` + height: (props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT }) } - function createAvailability(day: AvailabilityDay, time: Dayjs) { + const createAvailability = (day: AvailabilityDay, time: Dayjs) => { let fromTime = time; let toTime = time.add(props.availabilityDurationInMinutes, "minutes"); @@ -175,151 +283,47 @@ export default function AvailabilityPicker(props: { 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)} + className={"availability-parent-stack"} + onScroll={(_) => setGhostPreviewProps(null)} > { - days.map(a => generateDay(a)) + days.map(day => + , time: dayjs.Dayjs) => { + displayGhostPeriod(e, time); + }} + onMouseClickHalfhour={(day: AvailabilityDay, time: dayjs.Dayjs) => { + createAvailability(day, time); + }} + /> + ) } { (ghostPreviewProps !== null) && } ); -} \ No newline at end of file +} + +export default AvailabilityPicker; diff --git a/frontend/src/components/css/AvailabilityPicker.css b/frontend/src/components/css/AvailabilityPicker.css index f011ea4..f108096 100644 --- a/frontend/src/components/css/AvailabilityPicker.css +++ b/frontend/src/components/css/AvailabilityPicker.css @@ -43,4 +43,41 @@ div.full-hour:hover, div.half-hour:hover { div.full-hour:active, div.half-hour:active { background-color: var(--active-color); +} + +div.ghost-box { + position: absolute; + background-color: rgba(0, 255, 0, 0.1); + border: solid 1px; + border-color: #227722; + border-radius: 1; + margin: 0; + padding: 0; + pointer-events: none; +} + +p.time-text { + text-align: left; + font-size: 0.65em; + color: #ddd; +} + +div.day-card { + width: 120px; + min-width: 120px; + height: fit-content; + overflow: visible; +} + +div.availability-parent-box { + position: relative; + width: 100%; + height: auto; + overflow: hidden; +} + +div.availability-parent-stack { + width: 100%; + height: 500px; + overflow: scroll; } \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 673a3f3..6924b21 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -616,7 +616,7 @@ "@mui/system@^5.15.3": version "5.15.3" - resolved "https://registry.npmjs.org/@mui/system/-/system-5.15.3.tgz" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.3.tgz#062d0d6b5259c3dc0e1d4026b85ffcc3acf8637b" integrity sha512-ewVU4eRgo4VfNMGpO61cKlfWmH7l9s6rA8EknRzuMX3DbSLfmtW2WJJg6qPwragvpPIir0Pp/AdWVSDhyNy5Tw== dependencies: "@babel/runtime" "^7.23.6" @@ -1069,6 +1069,16 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +classname@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/classname/-/classname-0.0.0.tgz#43d171b484e354c7a293a5b78004df6852db20d6" + integrity sha512-kkhsspEJdUW+VhuvNzb2sQf0KbafDPfd36dB1qf03Uu42dWZwMQzaQuyNkaRr5ir0ZiAN0+TlH/EOOfwb/aaXg== + +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clsx@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz"