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 54cd657f..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'; @@ -6,8 +6,14 @@ import { useRouter } from 'next/router'; const dayjs = require('dayjs'); const isSameOrBefore = require('dayjs/plugin/isSameOrBefore'); const isBetween = require('dayjs/plugin/isBetween'); +const utc = require('dayjs/plugin/utc'); +const timezone = require('dayjs/plugin/timezone'); dayjs.extend(isSameOrBefore); dayjs.extend(isBetween); +dayjs.extend(utc); +dayjs.extend(timezone); + +import getSlots from '../../lib/slots' export default function Type(props) { // Initialise state @@ -29,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 = []; @@ -37,63 +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; - } - - // Until day end, push new times every x minutes - for (;i < props.user.endTime; i += parseInt(props.eventType.length)) { - times.push(dayjs(selectedDate).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute').add(props.eventType.length, 'minute').format("YYYY-MM-DD HH:mm:ss")); - } + 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); - } - }); + // 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).format("hh:mma")} +
+ + {dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}
); @@ -165,6 +180,7 @@ export async function getServerSideProps(context) { avatar: true, eventTypes: true, startTime: true, + timeZone: true, endTime: true } }); @@ -187,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 71236e2f..f291b595 100644 --- a/pages/api/availability/[user].ts +++ b/pages/api/availability/[user].ts @@ -12,7 +12,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) username: user, }, select: { - credentials: true + credentials: true, + timeZone: true } }); @@ -32,8 +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", + timeMin: req.query.dateFrom, + timeMax: req.query.dateTo, items: [{ "id": "primary" }] @@ -44,4 +45,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.status(200).json(availability); }); } -} \ No newline at end of file +} diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 6fee253b..eadfb61a 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -12,7 +12,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) username: user, }, select: { - credentials: true + credentials: true, + timeZone: true, } }); @@ -32,11 +33,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 'description': req.body.notes, 'start': { 'dateTime': req.body.start, - 'timeZone': 'Europe/London', + 'timeZone': currentUser.timeZone, }, 'end': { 'dateTime': req.body.end, - 'timeZone': 'Europe/London', + 'timeZone': currentUser.timeZone, }, 'attendees': [ {'email': req.body.email}, @@ -62,4 +63,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.status(200).json({message: 'Event created'}); }); } -} \ No newline at end of file +} diff --git a/pages/api/user/profile.ts b/pages/api/user/profile.ts index 0cd3b1e6..9ddbe984 100644 --- a/pages/api/user/profile.ts +++ b/pages/api/user/profile.ts @@ -25,7 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const username = req.body.username; const name = req.body.name; - const bio = req.body.description; + const description = req.body.description; + const timeZone = req.body.timeZone; const updateUser = await prisma.user.update({ where: { @@ -34,7 +35,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data: { username, name, - bio + bio: description, + timeZone: timeZone, }, }); diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 18a97c28..e2f8afa3 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -11,6 +11,7 @@ export default function Settings(props) { const usernameRef = useRef(); const nameRef = useRef(); const descriptionRef = useRef(); + const timezoneRef = useRef(); if (loading) { return

Loading...

; @@ -26,12 +27,13 @@ export default function Settings(props) { const enteredUsername = usernameRef.current.value; const enteredName = nameRef.current.value; const enteredDescription = descriptionRef.current.value; + const enteredTimezone = timezoneRef.current.value; // TODO: Add validation const response = await fetch('/api/user/profile', { method: 'PATCH', - body: JSON.stringify({username: enteredUsername, name: enteredName, description: enteredDescription}), + body: JSON.stringify({username: enteredUsername, name: enteredName, description: enteredDescription, timeZone: enteredTimezone}), headers: { 'Content-Type': 'application/json' } @@ -84,6 +86,511 @@ export default function Settings(props) {
+
+ +
+ +
+
@@ -147,7 +654,8 @@ export async function getServerSideProps(context) { name: true, email: true, bio: true, - avatar: true + avatar: true, + timeZone: true, } }); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b9724c95..02b85db9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,6 +35,7 @@ model User { password String? bio String? avatar String? + timeZone String @default("Europe/London") startTime Int @default(0) endTime Int @default(1440) createdDate DateTime @default(now()) @map(name: "created")