diff --git a/lib/slots.ts b/lib/slots.ts new file mode 100644 index 00000000..318b1bed --- /dev/null +++ b/lib/slots.ts @@ -0,0 +1,94 @@ +const dayjs = require("dayjs"); + +const isToday = require("dayjs/plugin/isToday"); +const utc = require("dayjs/plugin/utc"); +const timezone = require("dayjs/plugin/timezone"); + +dayjs.extend(isToday); +dayjs.extend(utc); +dayjs.extend(timezone); + +const getMinutesFromMidnight = (date) => { + return date.hour() * 60 + date.minute(); +}; + +const getSlots = ({ + calendarTimeZone, + eventLength, + selectedTimeZone, + selectedDate, + dayStartTime, + dayEndTime +}) => { + + if(!selectedDate) return [] + + const lowerBound = selectedDate.startOf("day"); + + // Simple case, same timezone + if (calendarTimeZone === selectedTimeZone) { + const slots = []; + const now = dayjs(); + for ( + let minutes = dayStartTime; + minutes <= dayEndTime - eventLength; + minutes += parseInt(eventLength, 10) + ) { + const slot = lowerBound.add(minutes, "minutes"); + if (slot > now) { + slots.push(slot); + } + } + return slots; + } + + const upperBound = selectedDate.endOf("day"); + + // We need to start generating slots from the start of the calendarTimeZone day + const startDateTime = lowerBound + .tz(calendarTimeZone) + .startOf("day") + .add(dayStartTime, "minutes"); + + let phase = 0; + if (startDateTime < lowerBound) { + // Getting minutes of the first event in the day of the chooser + const diff = lowerBound.diff(startDateTime, "minutes"); + + // finding first event + phase = diff + eventLength - (diff % eventLength); + } + + // We can stop as soon as the selectedTimeZone day ends + const endDateTime = upperBound + .tz(calendarTimeZone) + .subtract(eventLength, "minutes"); + + const maxMinutes = endDateTime.diff(startDateTime, "minutes"); + + const slots = []; + const now = dayjs(); + for ( + let minutes = phase; + minutes <= maxMinutes; + minutes += parseInt(eventLength, 10) + ) { + const slot = startDateTime.add(minutes, "minutes"); + + const minutesFromMidnight = getMinutesFromMidnight(slot); + + if ( + minutesFromMidnight < dayStartTime || + minutesFromMidnight > dayEndTime - eventLength || + slot < now + ) { + continue; + } + + slots.push(slot.tz(selectedTimeZone)); + } + + return slots; +}; + +export default getSlots diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index bb50d4db..55e3ebff 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useState, useMemo} from 'react'; import Head from 'next/head'; import Link from 'next/link'; import prisma from '../../lib/prisma'; @@ -13,6 +13,8 @@ dayjs.extend(isBetween); dayjs.extend(utc); dayjs.extend(timezone); +import getSlots from '../../lib/slots' + export default function Type(props) { // Initialise state const [selectedDate, setSelectedDate] = useState(''); @@ -33,6 +35,21 @@ export default function Type(props) { setSelectedMonth(selectedMonth - 1); } + // Need to define the bounds of the 24-hour window + const lowerBound = useMemo(() => { + if(!selectedDate) { + return + } + + return selectedDate.startOf('day') + }, [selectedDate]) + + const upperBound = useMemo(() => { + if(!selectedDate) return + + return selectedDate.endOf('day') + }, [selectedDate]) + // Set up calendar var daysInMonth = dayjs().month(selectedMonth).daysInMonth(); var days = []; @@ -41,76 +58,57 @@ export default function Type(props) { } const calendar = days.map((day) => - ); // Handle date change useEffect(async () => { + if(!selectedDate) { + return + } + setLoading(true); - const res = await fetch('/api/availability/' + user + '?date=' + dayjs(selectedDate).format("YYYY-MM-DD")); + const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`); const data = await res.json(); setBusy(data.primary.busy); setLoading(false); }, [selectedDate]); - // Set up timeslots - let times = []; - // If we're looking at availability throughout the current date, work out the current number of minutes elapsed throughout the day - if (selectedDate == dayjs().format("YYYY-MM-DD")) { - var i = (parseInt(dayjs().startOf('hour').format('H') * 60) + parseInt(dayjs().startOf('hour').format('m'))); - } else { - var i = props.user.startTime; - } - - // Adding first availability time - times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute')); - - // Until day end, push new times every x minutes - for (;i < props.user.endTime; i += parseInt(props.eventType.length)) { - times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute').add(props.eventType.length, 'minute')); - } + const times = getSlots({ + calendarTimeZone: props.user.timeZone, + selectedTimeZone: dayjs.tz.guess(), + eventLength: props.eventType.length, + selectedDate: selectedDate, + dayStartTime: props.user.startTime, + dayEndTime: props.user.endTime, + }) // Check for conflicts - for(i = times.length - 1; i >= 0; i -= 1) { - busy.forEach(busyTime => { - let startTime = dayjs(busyTime.start); - let endTime = dayjs(busyTime.end); + for(let i = times.length - 1; i >= 0; i -= 1) { + busy.forEach(busyTime => { + let startTime = dayjs(busyTime.start); + let endTime = dayjs(busyTime.end); - // Check if time has passed - if (dayjs(times[i]).isBefore(dayjs())) { - times.splice(i, 1); - } + // Check if start times are the same + if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { + times.splice(i, 1); + } - // Check if start times are the same - if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { - times.splice(i, 1); - } - - // Check if time is between start and end times - if (dayjs(times[i]).isBetween(startTime, endTime)) { - times.splice(i, 1); - } - }); - - // If event ends after endTime, remove the slot - if(dayjs(times[i]).add(props.eventType.length, 'minute') > dayjs(times[i]).hour(0).minute(0).add(props.user.endTime, 'minute') ) { - times.splice(i, 1); - } - - //If slot starts before startTime, remove it - if(dayjs(times[i]) < dayjs(times[i]).hour(0).minute(0).add(props.user.startTime, 'minute') ) { - times.splice(i, 1); - } + // Check if time is between start and end times + if (dayjs(times[i]).isBetween(startTime, endTime)) { + times.splice(i, 1); + } + }); } // Display available times const availableTimes = times.map((time) => -
- - {dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")} +
+ + {dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}
); @@ -205,4 +203,4 @@ export async function getServerSideProps(context) { eventType }, } -} \ No newline at end of file +} diff --git a/pages/api/availability/[user].ts b/pages/api/availability/[user].ts index 00a7a38c..f291b595 100644 --- a/pages/api/availability/[user].ts +++ b/pages/api/availability/[user].ts @@ -33,9 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const calendar = google.calendar({version: 'v3', auth}); calendar.freebusy.query({ requestBody: { - timeMin: req.query.date + "T00:00:00.00Z", - timeMax: req.query.date + "T23:59:59.59Z", - timeZone: currentUser.timeZone, + timeMin: req.query.dateFrom, + timeMax: req.query.dateTo, items: [{ "id": "primary" }]