error handling WIP
This commit is contained in:
parent
ded27d17ea
commit
931e6b26f1
4 changed files with 157 additions and 71 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue