From 931e6b26f1dd1e646b5be6cdafeddca74a6e3eff Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Sun, 20 Jun 2021 20:32:30 +0200 Subject: [PATCH] error handling WIP --- components/ui/Button.tsx | 9 ++-- pages/[user]/[type].tsx | 40 ++++++++++++++++-- pages/[user]/book.tsx | 91 ++++++++++++++++++++++++++-------------- pages/api/book/[user].ts | 88 +++++++++++++++++++++++++------------- 4 files changed, 157 insertions(+), 71 deletions(-) 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/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index aa57bd48..9c6a88c4 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -6,7 +6,14 @@ import { useRouter } from 'next/router'; import dayjs, { Dayjs } from 'dayjs'; import { Switch } from '@headlessui/react'; import TimezoneSelect from 'react-timezone-select'; -import { ClockIcon, GlobeIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'; +import { + ClockIcon, + GlobeIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + XCircleIcon, ExclamationIcon +} from '@heroicons/react/solid'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import isBetween from 'dayjs/plugin/isBetween'; import utc from 'dayjs/plugin/utc'; @@ -29,6 +36,7 @@ export default function Type(props) { const [selectedDate, setSelectedDate] = useState(); const [selectedMonth, setSelectedMonth] = useState(dayjs().month()); const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [is24h, setIs24h] = useState(false); const [busy, setBusy] = useState([]); @@ -72,9 +80,16 @@ export default function Type(props) { } setLoading(true); + setError(false); + const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`); - const busyTimes = await res.json(); - if (busyTimes.length > 0) setBusy(busyTimes); + if (res.ok) { + const busyTimes = await res.json(); + if (busyTimes.length > 0) setBusy(busyTimes); + } else { + setError(true); + } + setLoading(false); } changeDate(); @@ -340,7 +355,24 @@ export default function Type(props) { {dayjs(selectedDate).format("dddd DD MMMM YYYY")} - {!loading ? availableTimes :
} + {!loading && !error && availableTimes} + {loading &&
} + {error && +
+
+
+
+
+

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

+
+
+
}
)} diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index fb9165e9..1aeec756 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"; @@ -23,6 +23,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 || []; @@ -47,41 +49,51 @@ export default function Book(props) { }; const bookingHandler = event => { - event.preventDefault(); + const book = async () => { + setLoading(true); + setError(false); + 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: event.target.notes.value, + 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: event.target.notes.value, - timeZone: preferredTimeZone, - eventTypeId: props.eventType.id, - rescheduleUid: rescheduleUid - }; - - if (selectedLocation) { - payload['location'] = selectedLocation === LocationType.Phone ? event.target.phone.value : locationInfo(selectedLocation).address; - } - - 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' + if (selectedLocation) { + payload['location'] = selectedLocation === LocationType.Phone ? event.target.phone.value : locationInfo(selectedLocation).address; } - ); - let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=1&name=${payload.name}`; - if (payload['location']) { - successUrl += "&location=" + encodeURIComponent(payload['location']); + 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' + } + ); + + if (res.ok) { + let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=1&name=${payload.name}`; + if (payload['location']) { + successUrl += "&location=" + encodeURIComponent(payload['location']); + } + + await router.push(successUrl); + } else { + setLoading(false); + setError(true); + } } - router.push(successUrl); + event.preventDefault(); + book(); } return ( @@ -148,12 +160,27 @@ export default function Book(props) {
- + Cancel
+ {error &&
+
+
+
+
+

+ Could not {rescheduleUid ? 'reschedule' : 'book'} the meeting. Please try again or{' '} + + Contact {props.user.name} via e-mail + +

+
+
+
} diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 9c1f4f3e..9f802bc7 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -98,9 +98,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Use all integrations results = await async.mapLimit(currentUser.credentials, 5, async (credential) => { const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; - return await updateEvent(credential, bookingRefUid, appendLinksToEvents(evt)) + return updateEvent(credential, bookingRefUid, appendLinksToEvents(evt)) + .then(response => ({type: credential.type, success: true, response})) + .catch(e => { + console.error("createEvent failed", e) + return {type: credential.type, success: false} + }); }); + if (results.every(res => !res.success)) { + res.status(500).json({message: "Rescheduling failed"}); + return; + } + // Clone elements referencesToCreate = [...booking.references]; @@ -129,14 +139,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } else { // Schedule event results = await async.mapLimit(currentUser.credentials, 5, async (credential) => { - const response = await createEvent(credential, appendLinksToEvents(evt)); - return { - type: credential.type, - response - }; + return createEvent(credential, appendLinksToEvents(evt)) + .then(response => ({type: credential.type, success: true, response})) + .catch(e => { + console.error("createEvent failed", e) + return {type: credential.type, success: false} + }); }); - referencesToCreate = results.map((result => { + if (results.every(res => !res.success)) { + res.status(500).json({message: "Booking failed"}); + return; + } + + referencesToCreate = results.filter(res => res.success).map((result => { return { type: result.type, uid: result.response.id @@ -144,32 +160,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) })); } - await prisma.booking.create({ - data: { - uid: hashUID, - userId: currentUser.id, - references: { - create: referencesToCreate - }, - eventTypeId: eventType.id, + let booking; + try { + booking = await prisma.booking.create({ + data: { + uid: hashUID, + userId: currentUser.id, + references: { + create: referencesToCreate + }, + eventTypeId: eventType.id, - title: evt.title, - description: evt.description, - startTime: evt.startTime, - endTime: evt.endTime, + title: evt.title, + description: evt.description, + startTime: evt.startTime, + endTime: evt.endTime, - attendees: { - create: evt.attendees + attendees: { + create: evt.attendees + } } - } - }); - - // If one of the integrations allows email confirmations or no integrations are added, send it. - if (currentUser.credentials.length === 0 || !results.every((result) => result.disableConfirmationEmail)) { - await createConfirmBookedEmail( - evt, cancelLink, rescheduleLink - ); + }); + } catch (e) { + console.error("Error when saving booking to db", e); + res.status(500).json({message: "Booking already exists"}); + return; } - res.status(200).json(results); + try { + // If one of the integrations allows email confirmations or no integrations are added, send it. + // TODO: locally this is really slow (maybe only because the authentication for the mail service fails), so fire and forget would be nice here + if (currentUser.credentials.length === 0 || !results.every((result) => result.response.disableConfirmationEmail)) { + await createConfirmBookedEmail( + evt, cancelLink, rescheduleLink + ); + } + } catch (e) { + console.error("createConfirmBookedEmail failed", e) + } + + res.status(204).send({}); }