Completely rebuilt logic when dealing with timezones. Now all available events should appear when selecting a date.

This commit is contained in:
Leonardo Stenico 2021-04-17 02:08:35 +02:00
parent 96e2b762c6
commit 2b0e8bef7a
3 changed files with 145 additions and 54 deletions

94
lib/slots.ts Normal file
View file

@ -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

View file

@ -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) =>
<button key={day} onClick={(e) => setSelectedDate(dayjs().month(selectedMonth).date(day).format("YYYY-MM-DD"))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}>
<button key={day} onClick={(e) => setSelectedDate(dayjs().tz(dayjs.tz.guess()).month(selectedMonth).date(day))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}>
{day}
</button>
);
// 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) =>
<div key={time.format("YYYY-MM-DDTHH:mm:ss")}>
<Link href={`/${props.user.username}/book?date=${time.format("YYYY-MM-DDTHH:mm:ss")}&type=${props.eventType.id}`}>
<a key={time.format("YYYY-MM-DDTHH:mm:ss")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a>
<div key={dayjs(time).utc().format()}>
<Link href={`/${props.user.username}/book?date=${dayjs(time).utc().format()}&type=${props.eventType.id}`}>
<a key={dayjs(time).format("hh:mma")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a>
</Link>
</div>
);
@ -205,4 +203,4 @@ export async function getServerSideProps(context) {
eventType
},
}
}
}

View file

@ -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"
}]