Merge pull request #311 from Nico-J/feature/zoom-event-location
Added zoom as an event location and fixed linting
This commit is contained in:
commit
aa2e35d68e
3 changed files with 1048 additions and 764 deletions
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
export enum LocationType {
|
export enum LocationType {
|
||||||
InPerson = 'inPerson',
|
InPerson = "inPerson",
|
||||||
Phone = 'phone',
|
Phone = "phone",
|
||||||
GoogleMeet = 'integrations:google:meet'
|
GoogleMeet = "integrations:google:meet",
|
||||||
|
Zoom = "integrations:zoom",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,316 +1,415 @@
|
||||||
import Head from 'next/head';
|
import Head from "next/head";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import {useRouter} from 'next/router';
|
import { useRouter } from "next/router";
|
||||||
import {CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon} from '@heroicons/react/solid';
|
import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
|
||||||
import prisma from '../../lib/prisma';
|
import prisma from "../../lib/prisma";
|
||||||
import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from "dayjs/plugin/utc";
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import 'react-phone-number-input/style.css';
|
import "react-phone-number-input/style.css";
|
||||||
import PhoneInput from 'react-phone-number-input';
|
import PhoneInput from "react-phone-number-input";
|
||||||
import {LocationType} from '../../lib/location';
|
import { LocationType } from "../../lib/location";
|
||||||
import Avatar from '../../components/Avatar';
|
import Avatar from "../../components/Avatar";
|
||||||
import Button from '../../components/ui/Button';
|
import Button from "../../components/ui/Button";
|
||||||
import {EventTypeCustomInputType} from "../../lib/eventTypeInput";
|
import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
export default function Book(props) {
|
export default function Book(props: any): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { date, user, rescheduleUid } = router.query;
|
const { date, user, rescheduleUid } = router.query;
|
||||||
|
|
||||||
const [ is24h, setIs24h ] = useState(false);
|
const [is24h, setIs24h] = useState(false);
|
||||||
const [ preferredTimeZone, setPreferredTimeZone ] = useState('');
|
const [preferredTimeZone, setPreferredTimeZone] = useState("");
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [ error, setError ] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const locations = props.eventType.locations || [];
|
const locations = props.eventType.locations || [];
|
||||||
|
|
||||||
const [ selectedLocation, setSelectedLocation ] = useState<LocationType>(locations.length === 1 ? locations[0].type : '');
|
const [selectedLocation, setSelectedLocation] = useState<LocationType>(
|
||||||
const telemetry = useTelemetry();
|
locations.length === 1 ? locations[0].type : ""
|
||||||
useEffect(() => {
|
);
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
useEffect(() => {
|
||||||
|
setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess());
|
||||||
|
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
|
||||||
|
|
||||||
setPreferredTimeZone(localStorage.getItem('timeOption.preferredTimeZone') || dayjs.tz.guess());
|
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters()));
|
||||||
setIs24h(!!localStorage.getItem('timeOption.is24hClock'));
|
});
|
||||||
|
|
||||||
telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters()));
|
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
|
||||||
});
|
|
||||||
|
|
||||||
const locationInfo = (type: LocationType) => locations.find(
|
// TODO: Move to translations
|
||||||
(location) => location.type === type
|
const locationLabels = {
|
||||||
);
|
[LocationType.InPerson]: "In-person meeting",
|
||||||
|
[LocationType.Phone]: "Phone call",
|
||||||
|
[LocationType.GoogleMeet]: "Google Meet",
|
||||||
|
[LocationType.Zoom]: "Zoom Video",
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Move to translations
|
const bookingHandler = (event) => {
|
||||||
const locationLabels = {
|
const book = async () => {
|
||||||
[LocationType.InPerson]: 'In-person meeting',
|
setLoading(true);
|
||||||
[LocationType.Phone]: 'Phone call',
|
setError(false);
|
||||||
[LocationType.GoogleMeet]: 'Google Meet',
|
let notes = "";
|
||||||
};
|
if (props.eventType.customInputs) {
|
||||||
|
notes = props.eventType.customInputs
|
||||||
const bookingHandler = event => {
|
.map((input) => {
|
||||||
const book = async () => {
|
const data = event.target["custom_" + input.id];
|
||||||
setLoading(true);
|
if (data) {
|
||||||
setError(false);
|
if (input.type === EventTypeCustomInputType.Bool) {
|
||||||
let notes = "";
|
return input.label + "\n" + (data.value ? "Yes" : "No");
|
||||||
if (props.eventType.customInputs) {
|
} else {
|
||||||
notes = props.eventType.customInputs.map(input => {
|
return input.label + "\n" + data.value;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = await */fetch(
|
|
||||||
'/api/book/' + user,
|
|
||||||
{
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: 'POST'
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
// TODO When the endpoint is fixed, change this to await the result again
|
})
|
||||||
//if (res.ok) {
|
.join("\n\n");
|
||||||
let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`;
|
}
|
||||||
if (payload['location']) {
|
if (!!notes && !!event.target.notes.value) {
|
||||||
if (payload['location'].includes('integration')) {
|
notes += "\n\nAdditional notes:\n" + event.target.notes.value;
|
||||||
successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow.");
|
} else {
|
||||||
}
|
notes += event.target.notes.value;
|
||||||
else {
|
}
|
||||||
successUrl += "&location=" + encodeURIComponent(payload['location']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await router.push(successUrl);
|
const payload = {
|
||||||
/*} else {
|
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;
|
||||||
|
|
||||||
|
// Catches all other location types, such as Google Meet, Zoom etc.
|
||||||
|
default:
|
||||||
|
payload["location"] = selectedLocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
// TODO When the endpoint is fixed, change this to await the result again
|
||||||
|
//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);
|
setLoading(false);
|
||||||
setError(true);
|
setError(true);
|
||||||
}*/
|
}*/
|
||||||
}
|
};
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
book();
|
book();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{rescheduleUid ? 'Reschedule' : 'Confirm'} your {props.eventType.title} with {props.user.name || props.user.username} | Calendso</title>
|
<title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "}
|
||||||
</Head>
|
{props.user.name || props.user.username} | Calendso
|
||||||
|
</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
<main className="max-w-3xl mx-auto my-24">
|
<main className="max-w-3xl mx-auto my-24">
|
||||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div className="sm:flex px-4 py-5 sm:p-6">
|
<div className="sm:flex px-4 py-5 sm:p-6">
|
||||||
<div className="sm:w-1/2 sm:border-r">
|
<div className="sm:w-1/2 sm:border-r">
|
||||||
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
||||||
<h2 className="font-medium text-gray-500">{props.user.name}</h2>
|
<h2 className="font-medium text-gray-500">{props.user.name}</h2>
|
||||||
<h1 className="text-3xl font-semibold text-gray-800 mb-4">{props.eventType.title}</h1>
|
<h1 className="text-3xl font-semibold text-gray-800 mb-4">{props.eventType.title}</h1>
|
||||||
<p className="text-gray-500 mb-2">
|
<p className="text-gray-500 mb-2">
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{props.eventType.length} minutes
|
{props.eventType.length} minutes
|
||||||
</p>
|
</p>
|
||||||
{selectedLocation === LocationType.InPerson && <p className="text-gray-500 mb-2">
|
{selectedLocation === LocationType.InPerson && (
|
||||||
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<p className="text-gray-500 mb-2">
|
||||||
{locationInfo(selectedLocation).address}
|
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
</p>}
|
{locationInfo(selectedLocation).address}
|
||||||
<p className="text-blue-600 mb-4">
|
</p>
|
||||||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
)}
|
||||||
{preferredTimeZone && dayjs(date).tz(preferredTimeZone).format( (is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
<p className="text-blue-600 mb-4">
|
||||||
</p>
|
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
<p className="text-gray-600">{props.eventType.description}</p>
|
{preferredTimeZone &&
|
||||||
</div>
|
dayjs(date)
|
||||||
<div className="sm:w-1/2 pl-8 pr-4">
|
.tz(preferredTimeZone)
|
||||||
<form onSubmit={bookingHandler}>
|
.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
||||||
<div className="mb-4">
|
</p>
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
|
<p className="text-gray-600">{props.eventType.description}</p>
|
||||||
<div className="mt-1">
|
</div>
|
||||||
<input type="text" name="name" id="name" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Doe" defaultValue={props.booking ? props.booking.attendees[0].name : ''} />
|
<div className="sm:w-1/2 pl-8 pr-4">
|
||||||
</div>
|
<form onSubmit={bookingHandler}>
|
||||||
</div>
|
<div className="mb-4">
|
||||||
<div className="mb-4">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
|
Your name
|
||||||
<div className="mt-1">
|
</label>
|
||||||
<input type="email" name="email" id="email" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" defaultValue={props.booking ? props.booking.attendees[0].email : ''} />
|
<div className="mt-1">
|
||||||
</div>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
{locations.length > 1 && (
|
name="name"
|
||||||
<div className="mb-4">
|
id="name"
|
||||||
<span className="block text-sm font-medium text-gray-700">Location</span>
|
required
|
||||||
{locations.map( (location) => (
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
<label key={location.type} className="block">
|
placeholder="John Doe"
|
||||||
<input type="radio" required onChange={(e) => setSelectedLocation(e.target.value)} className="location" name="location" value={location.type} checked={selectedLocation === location.type} />
|
defaultValue={props.booking ? props.booking.attendees[0].name : ""}
|
||||||
<span className="text-sm ml-2">{locationLabels[location.type]}</span>
|
/>
|
||||||
</label>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedLocation === LocationType.Phone && (<div className="mb-4">
|
|
||||||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">Phone Number</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<PhoneInput name="phone" placeholder="Enter phone number" id="phone" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" onChange={() => {}} />
|
|
||||||
</div>
|
|
||||||
</div>)}
|
|
||||||
{props.eventType.customInputs && props.eventType.customInputs.sort((a,b) => a.id - b.id).map(input => (
|
|
||||||
<div className="mb-4">
|
|
||||||
{input.type !== EventTypeCustomInputType.Bool &&
|
|
||||||
<label htmlFor={input.label} className="block text-sm font-medium text-gray-700 mb-1">{input.label}</label>}
|
|
||||||
{input.type === EventTypeCustomInputType.TextLong &&
|
|
||||||
<textarea name={"custom_" + input.id} id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
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=""/>}
|
|
||||||
{input.type === EventTypeCustomInputType.Text &&
|
|
||||||
<input type="text" name={"custom_" + input.id} id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder=""/>}
|
|
||||||
{input.type === EventTypeCustomInputType.Number &&
|
|
||||||
<input type="number" name={"custom_" + input.id} id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder=""/>}
|
|
||||||
{input.type === EventTypeCustomInputType.Bool &&
|
|
||||||
<div className="flex items-center h-5">
|
|
||||||
<input type="checkbox" name={"custom_" + input.id} id={"custom_" + input.id}
|
|
||||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2"
|
|
||||||
placeholder=""/>
|
|
||||||
<label htmlFor={input.label} className="block text-sm font-medium text-gray-700">{input.label}</label>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">Additional notes</label>
|
|
||||||
<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 : ''}/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<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>
|
</div>
|
||||||
</main>
|
<div className="mb-4">
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
required
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
defaultValue={props.booking ? props.booking.attendees[0].email : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{locations.length > 1 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<span className="block text-sm font-medium text-gray-700">Location</span>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<label key={location.type} className="block">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSelectedLocation(e.target.value)}
|
||||||
|
className="location"
|
||||||
|
name="location"
|
||||||
|
value={location.type}
|
||||||
|
checked={selectedLocation === location.type}
|
||||||
|
/>
|
||||||
|
<span className="text-sm ml-2">{locationLabels[location.type]}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedLocation === LocationType.Phone && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<PhoneInput
|
||||||
|
name="phone"
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
id="phone"
|
||||||
|
required
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.eventType.customInputs &&
|
||||||
|
props.eventType.customInputs
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
.map((input) => (
|
||||||
|
<div className="mb-4" key={"input-" + input.label.toLowerCase}>
|
||||||
|
{input.type !== EventTypeCustomInputType.Bool && (
|
||||||
|
<label
|
||||||
|
htmlFor={input.label}
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{input.label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.TextLong && (
|
||||||
|
<textarea
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
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=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Text && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Number && (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Bool && (
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
<label htmlFor={input.label} className="block text-sm font-medium text-gray-700">
|
||||||
|
{input.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Additional notes
|
||||||
|
</label>
|
||||||
|
<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 : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start">
|
||||||
|
<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>
|
</div>
|
||||||
)
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username: context.query.user,
|
username: context.query.user,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
username: true,
|
username: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
bio: true,
|
||||||
|
avatar: true,
|
||||||
|
eventTypes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventType = await prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(context.query.type),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
length: true,
|
||||||
|
locations: true,
|
||||||
|
customInputs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let booking = null;
|
||||||
|
|
||||||
|
if (context.query.rescheduleUid) {
|
||||||
|
booking = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: context.query.rescheduleUid,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
description: true,
|
||||||
|
attendees: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
name: true,
|
name: true,
|
||||||
email:true,
|
},
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
eventTypes: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
|
||||||
where: {
|
|
||||||
id: parseInt(context.query.type),
|
|
||||||
},
|
},
|
||||||
select: {
|
},
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
slug: true,
|
|
||||||
description: true,
|
|
||||||
length: true,
|
|
||||||
locations: true,
|
|
||||||
customInputs: true,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let booking = null;
|
return {
|
||||||
|
props: {
|
||||||
if(context.query.rescheduleUid) {
|
user,
|
||||||
booking = await prisma.booking.findFirst({
|
eventType,
|
||||||
where: {
|
booking,
|
||||||
uid: context.query.rescheduleUid
|
},
|
||||||
},
|
};
|
||||||
select: {
|
|
||||||
description: true,
|
|
||||||
attendees: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
user,
|
|
||||||
eventType,
|
|
||||||
booking
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue