diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index f6f10574..2e1f5f3e 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -1,36 +1,37 @@ -import dayjs, {Dayjs} from "dayjs"; -import isBetween from 'dayjs/plugin/isBetween'; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; dayjs.extend(isBetween); -import {useEffect, useMemo, useState} from "react"; +import { useEffect, useState } from "react"; import getSlots from "../../lib/slots"; import Link from "next/link"; -import {timeZone} from "../../lib/clock"; -import {useRouter} from "next/router"; +import { timeZone } from "../../lib/clock"; +import { useRouter } from "next/router"; +import { ExclamationIcon } from "@heroicons/react/solid"; const AvailableTimes = (props) => { - const router = useRouter(); const { user, rescheduleUid } = router.query; const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); const times = getSlots({ - calendarTimeZone: props.user.timeZone, - selectedTimeZone: timeZone(), - eventLength: props.eventType.length, - selectedDate: props.date, - dayStartTime: props.user.startTime, - dayEndTime: props.user.endTime, - }); + calendarTimeZone: props.user.timeZone, + selectedTimeZone: timeZone(), + eventLength: props.eventType.length, + selectedDate: props.date, + dayStartTime: props.user.startTime, + dayEndTime: props.user.endTime, + }); const handleAvailableSlots = (busyTimes: []) => { // Check for conflicts for (let i = times.length - 1; i >= 0; i -= 1) { - busyTimes.forEach(busyTime => { - let startTime = dayjs(busyTime.start); - let endTime = dayjs(busyTime.end); + busyTimes.forEach((busyTime) => { + const startTime = dayjs(busyTime.start); + const endTime = dayjs(busyTime.end); // Check if start times are the same - if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { + if (dayjs(times[i]).format("HH:mm") == startTime.format("HH:mm")) { times.splice(i, 1); } @@ -40,12 +41,12 @@ const AvailableTimes = (props) => { } // Check if slot end time is between start and end time - if (dayjs(times[i]).add(props.eventType.length, 'minutes').isBetween(startTime, endTime)) { + if (dayjs(times[i]).add(props.eventType.length, "minutes").isBetween(startTime, endTime)) { times.splice(i, 1); } // Check if startTime is between slot - if (startTime.isBetween(dayjs(times[i]), dayjs(times[i]).add(props.eventType.length, 'minutes'))) { + if (startTime.isBetween(dayjs(times[i]), dayjs(times[i]).add(props.eventType.length, "minutes"))) { times.splice(i, 1); } }); @@ -57,31 +58,65 @@ const AvailableTimes = (props) => { // Re-render only when invitee changes date useEffect(() => { setLoaded(false); - fetch(`/api/availability/${user}?dateFrom=${props.date.startOf('day').utc().format()}&dateTo=${props.date.endOf('day').utc().format()}`) - .then( res => res.json()) - .then(handleAvailableSlots); + setError(false); + fetch( + `/api/availability/${user}?dateFrom=${props.date.startOf("day").utc().format()}&dateTo=${props.date + .endOf("day") + .utc() + .format()}` + ) + .then((res) => res.json()) + .then(handleAvailableSlots) + .catch((e) => { + console.error(e); + setError(true); + }); }, [props.date]); return (
- - {props.date.format("dddd DD MMMM YYYY")} - + {props.date.format("dddd DD MMMM YYYY")}
- { - loaded ? times.map((time) => + {!error && + loaded && + times.map((time) => (
- {dayjs(time).tz(timeZone()).format(props.timeFormat)} + href={ + `/${props.user.username}/book?date=${dayjs(time).utc().format()}&type=${props.eventType.id}` + + (rescheduleUid ? "&rescheduleUid=" + rescheduleUid : "") + }> + + {dayjs(time).tz(timeZone()).format(props.timeFormat)} +
- ) :
- } + ))} + {!error && !loaded &&
} + {error && ( +
+
+
+
+
+

+ Could not load the available time slots.{" "} + + Contact {props.user.name} via e-mail + +

+
+
+
+ )}
); -} +}; -export default AvailableTimes; \ No newline at end of file +export default AvailableTimes; diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index 647d633c..054bbd66 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -1,11 +1,10 @@ import { useState } from 'react'; export default function Button(props) { - const [loading, setLoading] = useState(false); return( - ); -} \ No newline at end of file +} diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 791235c5..4d8d7421 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -7,18 +7,51 @@ import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail" const translator = short(); +// eslint-disable-next-line @typescript-eslint/no-var-requires const { google } = require("googleapis"); +import prisma from "./prisma"; -const googleAuth = () => { - const { client_secret, client_id, redirect_uris } = JSON.parse( - process.env.GOOGLE_API_CREDENTIALS - ).web; - return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); +const googleAuth = (credential) => { + const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; + const myGoogleAuth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + myGoogleAuth.setCredentials(credential.key); + + const isExpired = () => myGoogleAuth.isTokenExpiring(); + + const refreshAccessToken = () => + myGoogleAuth + .refreshToken(credential.key.refresh_token) + .then((res) => { + const token = res.res.data; + credential.key.access_token = token.access_token; + credential.key.expiry_date = token.expiry_date; + return prisma.credential + .update({ + where: { + id: credential.id, + }, + data: { + key: credential.key, + }, + }) + .then(() => { + myGoogleAuth.setCredentials(credential.key); + return myGoogleAuth; + }); + }) + .catch((err) => { + console.error("Error refreshing google token", err); + return myGoogleAuth; + }); + + return { + getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()), + }; }; function handleErrorsJson(response) { if (!response.ok) { - response.json().then(console.log); + response.json().then((e) => console.error("O365 Error", e)); throw Error(response.statusText); } return response.json(); @@ -26,17 +59,17 @@ function handleErrorsJson(response) { function handleErrorsRaw(response) { if (!response.ok) { - response.text().then(console.log); + response.text().then((e) => console.error("O365 Error", e)); throw Error(response.statusText); } return response.text(); } const o365Auth = (credential) => { - const isExpired = (expiryDate) => expiryDate < +new Date(); + const isExpired = (expiryDate) => expiryDate < Math.round(+new Date() / 1000); - const refreshAccessToken = (refreshToken) => - fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + const refreshAccessToken = (refreshToken) => { + return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ @@ -50,11 +83,19 @@ const o365Auth = (credential) => { .then(handleErrorsJson) .then((responseBody) => { credential.key.access_token = responseBody.access_token; - credential.key.expiry_date = Math.round( - +new Date() / 1000 + responseBody.expires_in - ); - return credential.key.access_token; + credential.key.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); + return prisma.credential + .update({ + where: { + id: credential.id, + }, + data: { + key: credential.key, + }, + }) + .then(() => credential.key.access_token); }); + }; return { getToken: () => @@ -96,15 +137,11 @@ interface IntegrationCalendar { interface CalendarApiAdapter { createEvent(event: CalendarEvent): Promise; - updateEvent(uid: String, event: CalendarEvent); + updateEvent(uid: string, event: CalendarEvent); - deleteEvent(uid: String); + deleteEvent(uid: string); - getAvailability( - dateFrom, - dateTo, - selectedCalendars: IntegrationCalendar[] - ): Promise; + getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise; listCalendars(): Promise; } @@ -113,7 +150,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { const auth = o365Auth(credential); const translateEvent = (event: CalendarEvent) => { - let optional = {}; + const optional = {}; if (event.location) { optional.location = { displayName: event.location }; } @@ -171,12 +208,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { return { getAvailability: (dateFrom, dateTo, selectedCalendars) => { - const filter = - "?$filter=start/dateTime ge '" + - dateFrom + - "' and end/dateTime le '" + - dateTo + - "'"; + const filter = "?$filter=start/dateTime ge '" + dateFrom + "' and end/dateTime le '" + dateTo + "'"; return auth .getToken() .then((accessToken) => { @@ -195,10 +227,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { ).then((ids: string[]) => { const urls = ids.map( (calendarId) => - "https://graph.microsoft.com/v1.0/me/calendars/" + - calendarId + - "/events" + - filter + "https://graph.microsoft.com/v1.0/me/calendars/" + calendarId + "/events" + filter ); return Promise.all( urls.map((url) => @@ -217,9 +246,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { })) ) ) - ).then((results) => - results.reduce((acc, events) => acc.concat(events), []) - ); + ).then((results) => results.reduce((acc, events) => acc.concat(events), [])); }); }) .catch((err) => { @@ -242,7 +269,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { disableConfirmationEmail: true, })) ), - deleteEvent: (uid: String) => + deleteEvent: (uid: string) => auth.getToken().then((accessToken) => fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { method: "DELETE", @@ -251,7 +278,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { }, }).then(handleErrorsRaw) ), - updateEvent: (uid: String, event: CalendarEvent) => + updateEvent: (uid: string, event: CalendarEvent) => auth.getToken().then((accessToken) => fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { method: "PATCH", @@ -267,187 +294,188 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { }; const GoogleCalendar = (credential): CalendarApiAdapter => { - const myGoogleAuth = googleAuth(); - myGoogleAuth.setCredentials(credential.key); + const auth = googleAuth(credential); const integrationType = "google_calendar"; return { getAvailability: (dateFrom, dateTo, selectedCalendars) => - new Promise((resolve, reject) => { - const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); - calendar.calendarList - .list() - .then((cals) => { - const filteredItems = cals.data.items.filter( - (i) => - selectedCalendars.findIndex((e) => e.externalId === i.id) > -1 - ); - if (filteredItems.length == 0 && selectedCalendars.length > 0) { - // Only calendars of other integrations selected - resolve([]); - } - calendar.freebusy.query( - { - requestBody: { - timeMin: dateFrom, - timeMax: dateTo, - items: - filteredItems.length > 0 ? filteredItems : cals.data.items, + new Promise((resolve, reject) => + auth.getToken().then((myGoogleAuth) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + const selectedCalendarIds = selectedCalendars + .filter((e) => e.integration === integrationType) + .map((e) => e.externalId); + if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) { + // Only calendars of other integrations selected + resolve([]); + return; + } + + (selectedCalendarIds.length == 0 + ? calendar.calendarList.list().then((cals) => cals.data.items.map((cal) => cal.id)) + : Promise.resolve(selectedCalendarIds) + ) + .then((calsIds) => { + calendar.freebusy.query( + { + requestBody: { + timeMin: dateFrom, + timeMax: dateTo, + items: calsIds.map((id) => ({ id: id })), + }, }, - }, - (err, apires) => { - if (err) { - reject(err); + (err, apires) => { + if (err) { + reject(err); + } + resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"])); } - - resolve( - Object.values(apires.data.calendars).flatMap( - (item) => item["busy"] - ) - ); - } - ); - }) - .catch((err) => { - reject(err); - }); - }), + ); + }) + .catch((err) => { + console.error("There was an error contacting google calendar service: ", err); + reject(err); + }); + }) + ), createEvent: (event: CalendarEvent) => - new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [{ method: "email", minutes: 60 }], - }, - }; + new Promise((resolve, reject) => + auth.getToken().then((myGoogleAuth) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [{ method: "email", minutes: 60 }], + }, + }; - if (event.location) { - payload["location"] = event.location; - } - - if (event.conferenceData) { - payload["conferenceData"] = event.conferenceData; - } - - const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); - calendar.events.insert( - { - auth: myGoogleAuth, - calendarId: "primary", - resource: payload, - conferenceDataVersion: 1, - }, - function (err, event) { - if (err) { - console.log( - "There was an error contacting the Calendar service: " + err - ); - return reject(err); - } - return resolve(event.data); + if (event.location) { + payload["location"] = event.location; } - ); - }), - updateEvent: (uid: String, event: CalendarEvent) => - new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [{ method: "email", minutes: 60 }], - }, - }; - if (event.location) { - payload["location"] = event.location; - } + if (event.conferenceData) { + payload["conferenceData"] = event.conferenceData; + } - const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); - calendar.events.update( - { - auth: myGoogleAuth, - calendarId: "primary", - eventId: uid, - sendNotifications: true, - sendUpdates: "all", - resource: payload, - }, - function (err, event) { - if (err) { - console.log( - "There was an error contacting the Calendar service: " + err - ); - return reject(err); + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.insert( + { + auth: myGoogleAuth, + calendarId: "primary", + resource: payload, + }, + function (err, event) { + if (err) { + console.error("There was an error contacting google calendar service: ", err); + return reject(err); + } + return resolve(event.data); } - return resolve(event.data); + ); + }) + ), + updateEvent: (uid: string, event: CalendarEvent) => + new Promise((resolve, reject) => + auth.getToken().then((myGoogleAuth) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [{ method: "email", minutes: 60 }], + }, + }; + + if (event.location) { + payload["location"] = event.location; } - ); - }), - deleteEvent: (uid: String) => - new Promise((resolve, reject) => { - const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); - calendar.events.delete( - { - auth: myGoogleAuth, - calendarId: "primary", - eventId: uid, - sendNotifications: true, - sendUpdates: "all", - }, - function (err, event) { - if (err) { - console.log( - "There was an error contacting the Calendar service: " + err - ); - return reject(err); + + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.update( + { + auth: myGoogleAuth, + calendarId: "primary", + eventId: uid, + sendNotifications: true, + sendUpdates: "all", + resource: payload, + }, + function (err, event) { + if (err) { + console.error("There was an error contacting google calendar service: ", err); + return reject(err); + } + return resolve(event.data); } - return resolve(event.data); - } - ); - }), + ); + }) + ), + deleteEvent: (uid: string) => + new Promise((resolve, reject) => + auth.getToken().then((myGoogleAuth) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.delete( + { + auth: myGoogleAuth, + calendarId: "primary", + eventId: uid, + sendNotifications: true, + sendUpdates: "all", + }, + function (err, event) { + if (err) { + console.error("There was an error contacting google calendar service: ", err); + return reject(err); + } + return resolve(event.data); + } + ); + }) + ), listCalendars: () => - new Promise((resolve, reject) => { - const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); - calendar.calendarList - .list() - .then((cals) => { - resolve( - cals.data.items.map((cal) => { - const calendar: IntegrationCalendar = { - externalId: cal.id, - integration: integrationType, - name: cal.summary, - primary: cal.primary, - }; - return calendar; - }) - ); - }) - .catch((err) => { - reject(err); - }); - }), + new Promise((resolve, reject) => + auth.getToken().then((myGoogleAuth) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.calendarList + .list() + .then((cals) => { + resolve( + cals.data.items.map((cal) => { + const calendar: IntegrationCalendar = { + externalId: cal.id, + integration: integrationType, + name: cal.summary, + primary: cal.primary, + }; + return calendar; + }) + ); + }) + .catch((err) => { + console.error("There was an error contacting google calendar service: ", err); + reject(err); + }); + }) + ), }; }; @@ -466,43 +494,37 @@ const calendars = (withCredentials): CalendarApiAdapter[] => }) .filter(Boolean); -const getBusyCalendarTimes = ( - withCredentials, - dateFrom, - dateTo, - selectedCalendars -) => +const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => Promise.all( - calendars(withCredentials).map((c) => - c.getAvailability(dateFrom, dateTo, selectedCalendars) - ) + calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)) ).then((results) => { return results.reduce((acc, availability) => acc.concat(availability), []); }); const listCalendars = (withCredentials) => - Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then( - (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) + Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then((results) => + results.reduce((acc, calendars) => acc.concat(calendars), []) ); -const createEvent = async ( - credential, - calEvent: CalendarEvent -): Promise => { - const uid: string = translator.fromUUID( - uuidv5(JSON.stringify(calEvent), uuidv5.URL) - ); +const createEvent = async (credential, calEvent: CalendarEvent): Promise => { + const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); - const creationResult = credential - ? await calendars([credential])[0].createEvent(calEvent) - : null; + const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; const organizerMail = new EventOrganizerMail(calEvent, uid); const attendeeMail = new EventAttendeeMail(calEvent, uid); - await organizerMail.sendEmail(); + try { + await organizerMail.sendEmail(); + } catch (e) { + console.error("organizerMail.sendEmail failed", e); + } if (!creationResult || !creationResult.disableConfirmationEmail) { - await attendeeMail.sendEmail(); + try { + await attendeeMail.sendEmail(); + } catch (e) { + console.error("attendeeMail.sendEmail failed", e); + } } return { @@ -511,14 +533,8 @@ const createEvent = async ( }; }; -const updateEvent = async ( - credential, - uidToUpdate: String, - calEvent: CalendarEvent -): Promise => { - const newUid: string = translator.fromUUID( - uuidv5(JSON.stringify(calEvent), uuidv5.URL) - ); +const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEvent): Promise => { + const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) @@ -526,10 +542,18 @@ const updateEvent = async ( const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); - await organizerMail.sendEmail(); + try { + await organizerMail.sendEmail(); + } catch (e) { + console.error("organizerMail.sendEmail failed", e); + } if (!updateResult || !updateResult.disableConfirmationEmail) { - await attendeeMail.sendEmail(); + try { + await attendeeMail.sendEmail(); + } catch (e) { + console.error("attendeeMail.sendEmail failed", e); + } } return { @@ -538,7 +562,7 @@ const updateEvent = async ( }; }; -const deleteEvent = (credential, uid: String): Promise => { +const deleteEvent = (credential, uid: string): Promise => { if (credential) { return calendars([credential])[0].deleteEvent(uid); } diff --git a/lib/videoClient.ts b/lib/videoClient.ts index b359e83a..0e171ac6 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -193,10 +193,18 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise const organizerMail = new VideoEventOrganizerMail(calEvent, uid, videoCallData); const attendeeMail = new VideoEventAttendeeMail(calEvent, uid, videoCallData); - await organizerMail.sendEmail(); + try { + await organizerMail.sendEmail(); + } catch (e) { + console.error("organizerMail.sendEmail failed", e) + } if (!creationResult || !creationResult.disableConfirmationEmail) { - await attendeeMail.sendEmail(); + try { + await attendeeMail.sendEmail(); + } catch (e) { + console.error("attendeeMail.sendEmail failed", e) + } } return { @@ -216,10 +224,18 @@ const updateMeeting = async (credential, uidToUpdate: String, calEvent: Calendar const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); - await organizerMail.sendEmail(); + try { + await organizerMail.sendEmail(); + } catch (e) { + console.error("organizerMail.sendEmail failed", e) + } if (!updateResult || !updateResult.disableConfirmationEmail) { - await attendeeMail.sendEmail(); + try { + await attendeeMail.sendEmail(); + } catch (e) { + console.error("attendeeMail.sendEmail failed", e) + } } return { diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 31b7f23a..0266ef62 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -1,7 +1,7 @@ import Head from 'next/head'; import Link from 'next/link'; import {useRouter} from 'next/router'; -import {CalendarIcon, ClockIcon, LocationMarkerIcon} from '@heroicons/react/solid'; +import {CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon} from '@heroicons/react/solid'; import prisma from '../../lib/prisma'; import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry"; import {useEffect, useState} from "react"; @@ -24,6 +24,8 @@ export default function Book(props) { const [ is24h, setIs24h ] = useState(false); const [ preferredTimeZone, setPreferredTimeZone ] = useState(''); + const [ loading, setLoading ] = useState(false); + const [ error, setError ] = useState(false); const locations = props.eventType.locations || []; @@ -48,78 +50,88 @@ export default function Book(props) { [LocationType.GoogleMeet]: 'Google Meet', }; - const bookingHandler = (event) => { - event.preventDefault(); - - let notes = ""; - if (props.eventType.customInputs) { - notes = props.eventType.customInputs.map(input => { - const data = event.target["custom_" + input.id]; - if (!!data) { - if (input.type === EventTypeCustomInputType.Bool) { - return input.label + "\n" + (data.value ? "Yes" : "No") - } else { - return input.label + "\n" + data.value + const bookingHandler = event => { + const book = async () => { + setLoading(true); + setError(false); + let notes = ""; + if (props.eventType.customInputs) { + notes = props.eventType.customInputs.map(input => { + const data = event.target["custom_" + input.id]; + if (!!data) { + if (input.type === EventTypeCustomInputType.Bool) { + return input.label + "\n" + (data.value ? "Yes" : "No") + } else { + return input.label + "\n" + data.value + } } - } - }).join("\n\n") - } - if (!!notes && !!event.target.notes.value) { - notes += "\n\nAdditional notes:\n" + event.target.notes.value; - } else { - notes += event.target.notes.value; - } + }).join("\n\n") + } + if (!!notes && !!event.target.notes.value) { + notes += "\n\nAdditional notes:\n" + event.target.notes.value; + } else { + notes += event.target.notes.value; + } - let payload = { - start: dayjs(date).format(), - end: dayjs(date).add(props.eventType.length, 'minute').format(), - name: event.target.name.value, - email: event.target.email.value, - notes: notes, - timeZone: preferredTimeZone, - eventTypeId: props.eventType.id, - rescheduleUid: rescheduleUid - }; + let payload = { + start: dayjs(date).format(), + end: dayjs(date).add(props.eventType.length, 'minute').format(), + name: event.target.name.value, + email: event.target.email.value, + notes: notes, + timeZone: preferredTimeZone, + eventTypeId: props.eventType.id, + rescheduleUid: rescheduleUid + }; if (selectedLocation) { switch (selectedLocation) { case LocationType.Phone: payload['location'] = event.target.phone.value break - + case LocationType.InPerson: payload['location'] = locationInfo(selectedLocation).address break - + case LocationType.GoogleMeet: payload['location'] = LocationType.GoogleMeet break } } - telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); - const res = fetch( - '/api/book/' + user, - { - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json' - }, - method: 'POST' - } - ); + telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); + const res = await fetch( + '/api/book/' + user, + { + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + } + ); - let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; - if (payload['location']) { - if (payload['location'].includes('integration')) { - successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); - } - else { - successUrl += "&location=" + encodeURIComponent(payload['location']); + if (res.ok) { + let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; + if (payload['location']) { + if (payload['location'].includes('integration')) { + successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); + } + else { + successUrl += "&location=" + encodeURIComponent(payload['location']); + } + } + + await router.push(successUrl); + } else { + setLoading(false); + setError(true); } } - router.push(successUrl); + event.preventDefault(); + book(); } return ( @@ -215,12 +227,27 @@ export default function Book(props) {