error handling WIP

This commit is contained in:
Malte Delfs 2021-06-20 20:32:30 +02:00
parent ded27d17ea
commit 931e6b26f1
4 changed files with 157 additions and 71 deletions

View file

@ -1,11 +1,10 @@
import { useState } from 'react';
export default function Button(props) {
const [loading, setLoading] = useState(false);
return(
<button type="submit" className="btn btn-primary" onClick={setLoading}>
{!loading && props.children}
{loading &&
<button type="submit" className="btn btn-primary">
{!props.loading && props.children}
{props.loading &&
<svg className="animate-spin mx-4 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
@ -13,4 +12,4 @@ export default function Button(props) {
}
</button>
);
}
}

View file

@ -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<Dayjs>();
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")}
</span>
</div>
{!loading ? availableTimes : <div className="loader"></div>}
{!loading && !error && availableTimes}
{loading && <div className="loader"/>}
{error &&
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
Could not load the available time slots.{' '}
<a href={"mailto:" + props.user.email} className="font-medium underline text-yellow-700 hover:text-yellow-600">
Contact {props.user.name} via e-mail
</a>
</p>
</div>
</div>
</div>}
</div>
)}
</div>

View file

@ -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) {
<textarea name="notes" id="notes" rows={3} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Please share anything that will help prepare for our meeting." defaultValue={props.booking ? props.booking.description : ''}></textarea>
</div>
<div className="flex items-start">
<Button type="submit" className="btn btn-primary">{rescheduleUid ? 'Reschedule' : 'Confirm'}</Button>
<Button type="submit" loading={loading} className="btn btn-primary">{rescheduleUid ? 'Reschedule' : 'Confirm'}</Button>
<Link href={"/" + props.user.username + "/" + props.eventType.slug + (rescheduleUid ? "?rescheduleUid=" + rescheduleUid : "")}>
<a className="ml-2 btn btn-white">Cancel</a>
</Link>
</div>
</form>
{error && <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
Could not {rescheduleUid ? 'reschedule' : 'book'} the meeting. Please try again or{' '}
<a href={"mailto:" + props.user.email} className="font-medium underline text-yellow-700 hover:text-yellow-600">
Contact {props.user.name} via e-mail
</a>
</p>
</div>
</div>
</div>}
</div>
</div>
</div>

View file

@ -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({});
}