Completely rebuilt logic when dealing with timezones. Now all available events should appear when selecting a date.
This commit is contained in:
parent
96e2b762c6
commit
2b0e8bef7a
3 changed files with 145 additions and 54 deletions
94
lib/slots.ts
Normal file
94
lib/slots.ts
Normal 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
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}]
|
||||
|
|
Loading…
Reference in a new issue