Added zoom as an event location and fixed ESLint

This commit is contained in:
nicolas 2021-06-25 00:26:55 +02:00
parent dada6a3a79
commit bc47975316
3 changed files with 1048 additions and 764 deletions

View File

@ -1,7 +1,6 @@
export enum LocationType {
InPerson = 'inPerson',
Phone = 'phone',
GoogleMeet = 'integrations:google:meet'
InPerson = "inPerson",
Phone = "phone",
GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom",
}

View File

@ -1,71 +1,73 @@
import Head from 'next/head';
import Link from 'next/link';
import {useRouter} from 'next/router';
import {CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon} from '@heroicons/react/solid';
import prisma from '../../lib/prisma';
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
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";
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import 'react-phone-number-input/style.css';
import PhoneInput from 'react-phone-number-input';
import {LocationType} from '../../lib/location';
import Avatar from '../../components/Avatar';
import Button from '../../components/ui/Button';
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import "react-phone-number-input/style.css";
import PhoneInput from "react-phone-number-input";
import { LocationType } from "../../lib/location";
import Avatar from "../../components/Avatar";
import Button from "../../components/ui/Button";
import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
dayjs.extend(utc);
dayjs.extend(timezone);
export default function Book(props) {
export default function Book(props: any): JSX.Element {
const router = useRouter();
const { date, user, rescheduleUid } = router.query;
const [is24h, setIs24h] = useState(false);
const [ preferredTimeZone, setPreferredTimeZone ] = useState('');
const [preferredTimeZone, setPreferredTimeZone] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const locations = props.eventType.locations || [];
const [ selectedLocation, setSelectedLocation ] = useState<LocationType>(locations.length === 1 ? locations[0].type : '');
const [selectedLocation, setSelectedLocation] = useState<LocationType>(
locations.length === 1 ? locations[0].type : ""
);
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());
setIs24h(!!localStorage.getItem('timeOption.is24hClock'));
telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters()));
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters()));
});
const locationInfo = (type: LocationType) => locations.find(
(location) => location.type === type
);
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
// TODO: Move to translations
const locationLabels = {
[LocationType.InPerson]: 'In-person meeting',
[LocationType.Phone]: 'Phone call',
[LocationType.GoogleMeet]: 'Google Meet',
[LocationType.InPerson]: "In-person meeting",
[LocationType.Phone]: "Phone call",
[LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video",
};
const bookingHandler = event => {
const bookingHandler = (event) => {
const book = async () => {
setLoading(true);
setError(false);
let notes = "";
if (props.eventType.customInputs) {
notes = props.eventType.customInputs.map(input => {
notes = props.eventType.customInputs
.map((input) => {
const data = event.target["custom_" + input.id];
if (!!data) {
if (data) {
if (input.type === EventTypeCustomInputType.Bool) {
return input.label + "\n" + (data.value ? "Yes" : "No")
return input.label + "\n" + (data.value ? "Yes" : "No");
} else {
return input.label + "\n" + data.value
return input.label + "\n" + data.value;
}
}
}).join("\n\n")
})
.join("\n\n");
}
if (!!notes && !!event.target.notes.value) {
notes += "\n\nAdditional notes:\n" + event.target.notes.value;
@ -73,54 +75,54 @@ export default function Book(props) {
notes += event.target.notes.value;
}
let payload = {
const payload = {
start: dayjs(date).format(),
end: dayjs(date).add(props.eventType.length, 'minute').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
rescheduleUid: rescheduleUid,
};
if (selectedLocation) {
switch (selectedLocation) {
case LocationType.Phone:
payload['location'] = event.target.phone.value
break
payload["location"] = event.target.phone.value;
break;
case LocationType.InPerson:
payload['location'] = locationInfo(selectedLocation).address
break
payload["location"] = locationInfo(selectedLocation).address;
break;
case LocationType.GoogleMeet:
payload['location'] = LocationType.GoogleMeet
break
// Catches all other location types, such as Google Meet, Zoom etc.
default:
payload["location"] = selectedLocation;
}
}
telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters()));
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())
);
/*const res = await */fetch(
'/api/book/' + user,
{
/*const res = await */ fetch("/api/book/" + user, {
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json",
},
method: 'POST'
}
);
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')) {
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']);
} else {
successUrl += "&location=" + encodeURIComponent(payload["location"]);
}
}
@ -129,16 +131,19 @@ export default function Book(props) {
setLoading(false);
setError(true);
}*/
}
};
event.preventDefault();
book();
}
};
return (
<div>
<Head>
<title>{rescheduleUid ? 'Reschedule' : 'Confirm'} your {props.eventType.title} with {props.user.name || props.user.username} | Calendso</title>
<title>
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "}
{props.user.name || props.user.username} | Calendso
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
@ -153,28 +158,53 @@ export default function Book(props) {
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{props.eventType.length} minutes
</p>
{selectedLocation === LocationType.InPerson && <p className="text-gray-500 mb-2">
{selectedLocation === LocationType.InPerson && (
<p className="text-gray-500 mb-2">
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{locationInfo(selectedLocation).address}
</p>}
</p>
)}
<p className="text-blue-600 mb-4">
<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")}
{preferredTimeZone &&
dayjs(date)
.tz(preferredTimeZone)
.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
</p>
<p className="text-gray-600">{props.eventType.description}</p>
</div>
<div className="sm:w-1/2 pl-8 pr-4">
<form onSubmit={bookingHandler}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Your name
</label>
<div className="mt-1">
<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 : ''} />
<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>
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
<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 : ''} />
<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 && (
@ -182,79 +212,148 @@ export default function Book(props) {
<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} />
<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" onChange={() => {}} />
</div>
</div>)}
{props.eventType.customInputs && props.eventType.customInputs.sort((a,b) => a.id - b.id).map(input => (
{selectedLocation === LocationType.Phone && (
<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}
<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}
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}
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 &&
placeholder=""
/>
)}
{input.type === EventTypeCustomInputType.Bool && (
<div className="flex items-center h-5">
<input type="checkbox" name={"custom_" + input.id} id={"custom_" + input.id}
<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>}
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 : ''}/>
<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 : "")}>
<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">
{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">
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) {
@ -268,8 +367,8 @@ export async function getServerSideProps(context) {
email: true,
bio: true,
avatar: true,
eventTypes: true
}
eventTypes: true,
},
});
const eventType = await prisma.eventType.findUnique({
@ -284,7 +383,7 @@ export async function getServerSideProps(context) {
length: true,
locations: true,
customInputs: true,
}
},
});
let booking = null;
@ -292,17 +391,17 @@ export async function getServerSideProps(context) {
if (context.query.rescheduleUid) {
booking = await prisma.booking.findFirst({
where: {
uid: context.query.rescheduleUid
uid: context.query.rescheduleUid,
},
select: {
description: true,
attendees: {
select: {
email: true,
name: true
}
}
}
name: true,
},
},
},
});
}
@ -310,7 +409,7 @@ export async function getServerSideProps(context) {
props: {
user,
eventType,
booking
booking,
},
}
};
}

View File

@ -1,40 +1,37 @@
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useRef, useState, useEffect } from 'react';
import Select, { OptionBase } from 'react-select';
import prisma from '../../../lib/prisma';
import {LocationType} from '../../../lib/location';
import Shell from '../../../components/Shell';
import { useSession, getSession } from 'next-auth/client';
import {
LocationMarkerIcon,
PlusCircleIcon,
XIcon,
PhoneIcon,
} from '@heroicons/react/outline';
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import Select, { OptionBase } from "react-select";
import prisma from "../../../lib/prisma";
import { LocationType } from "../../../lib/location";
import Shell from "../../../components/Shell";
import { getSession, useSession } from "next-auth/client";
import { LocationMarkerIcon, PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline";
import { EventTypeCustomInput, EventTypeCustomInputType } from "../../../lib/eventTypeInput";
import { PlusIcon } from "@heroicons/react/solid";
export default function EventType(props) {
export default function EventType(props: any): JSX.Element {
const router = useRouter();
const inputOptions: OptionBase[] = [
{ value: EventTypeCustomInputType.Text, label: 'Text' },
{ value: EventTypeCustomInputType.TextLong, label: 'Multiline Text' },
{ value: EventTypeCustomInputType.Number, label: 'Number', },
{ value: EventTypeCustomInputType.Bool, label: 'Checkbox', },
]
{ value: EventTypeCustomInputType.Text, label: "Text" },
{ value: EventTypeCustomInputType.TextLong, label: "Multiline Text" },
{ value: EventTypeCustomInputType.Number, label: "Number" },
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
];
const [ session, loading ] = useSession();
const [, loading] = useSession();
const [showLocationModal, setShowLocationModal] = useState(false);
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined);
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
const [locations, setLocations] = useState(props.eventType.locations || []);
const [ customInputs, setCustomInputs ] = useState<EventTypeCustomInput[]>(props.eventType.customInputs.sort((a, b) => a.id - b.id) || []);
const locationOptions = props.locationOptions
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
props.eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
const locationOptions = props.locationOptions;
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
@ -58,35 +55,45 @@ export default function EventType(props) {
const enteredEventName = eventNameRef.current.value;
// TODO: Add validation
const response = await fetch('/api/availability/eventtype', {
method: 'PATCH',
body: JSON.stringify({id: props.eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden, locations, eventName: enteredEventName, customInputs }),
await fetch("/api/availability/eventtype", {
method: "PATCH",
body: JSON.stringify({
id: props.eventType.id,
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
hidden: enteredIsHidden,
locations,
eventName: enteredEventName,
customInputs,
}),
headers: {
'Content-Type': 'application/json'
}
"Content-Type": "application/json",
},
});
router.push('/availability');
router.push("/availability");
}
async function deleteEventTypeHandler(event) {
event.preventDefault();
const response = await fetch('/api/availability/eventtype', {
method: 'DELETE',
await fetch("/api/availability/eventtype", {
method: "DELETE",
body: JSON.stringify({ id: props.eventType.id }),
headers: {
'Content-Type': 'application/json'
}
"Content-Type": "application/json",
},
});
router.push('/availability');
router.push("/availability");
}
const openLocationModal = (type: LocationType) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
setShowLocationModal(true);
}
};
const closeLocationModal = () => {
setSelectedLocation(undefined);
@ -101,9 +108,9 @@ export default function EventType(props) {
const openEditCustomModel = (customInput: EventTypeCustomInput) => {
setSelectedCustomInput(customInput);
setSelectedInputOption(inputOptions.find(e => e.value === customInput.type));
setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type));
setShowAddCustomModal(true);
}
};
const LocationOptions = () => {
if (!selectedLocation) {
@ -111,26 +118,31 @@ export default function EventType(props) {
}
switch (selectedLocation.value) {
case LocationType.InPerson:
const address = locations.find(
(location) => location.type === LocationType.InPerson
)?.address;
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">Set an address or place</label>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
Set an address or place
</label>
<div className="mt-1">
<input type="text" name="address" id="address" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" defaultValue={address} />
<input
type="text"
name="address"
id="address"
required
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
defaultValue={locations.find((location) => location.type === LocationType.InPerson)?.address}
/>
</div>
</div>
)
);
case LocationType.Phone:
return (
<p className="text-sm">Calendso will ask your invitee to enter a phone number before scheduling.</p>
)
);
case LocationType.GoogleMeet:
return (
<p className="text-sm">Calendso will provide a Google Meet location.</p>
)
return <p className="text-sm">Calendso will provide a Google Meet location.</p>;
case LocationType.Zoom:
return <p className="text-sm">Calendso will provide a Zoom meeting URL.</p>;
}
return null;
};
@ -145,7 +157,7 @@ export default function EventType(props) {
const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type);
if (existingIdx !== -1) {
let copy = locations;
const copy = locations;
copy[existingIdx] = { ...locations[existingIdx], ...details };
setLocations(copy);
} else {
@ -165,11 +177,11 @@ export default function EventType(props) {
const customInput: EventTypeCustomInput = {
label: e.target.label.value,
required: e.target.required.checked,
type: e.target.type.value
type: e.target.type.value,
};
if (!!e.target.id?.value) {
const index = customInputs.findIndex(inp => inp.id === +e.target.id?.value);
if (e.target.id?.value) {
const index = customInputs.findIndex((inp) => inp.id === +e.target.id?.value);
if (index >= 0) {
const input = customInputs[index];
input.label = customInput.label;
@ -185,12 +197,12 @@ export default function EventType(props) {
const removeCustom = (customInput, e) => {
e.preventDefault();
const index = customInputs.findIndex(inp => inp.id === customInput.id);
const index = customInputs.findIndex((inp) => inp.id === customInput.id);
if (index >= 0) {
customInputs.splice(index, 1);
setCustomInputs([...customInputs]);
}
}
};
return (
<div>
@ -198,20 +210,33 @@ export default function EventType(props) {
<title>{props.eventType.title} | Event Type | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading={'Event Type - ' + props.eventType.title}>
<Shell heading={"Event Type - " + props.eventType.title}>
<div>
<div className="mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<form onSubmit={updateEventTypeHandler}>
<div className="mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<div className="mt-1">
<input ref={titleRef} type="text" name="title" id="title" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Quick Chat" defaultValue={props.eventType.title} />
<input
ref={titleRef}
type="text"
name="title"
id="title"
required
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Quick Chat"
defaultValue={props.eventType.title}
/>
</div>
</div>
<div className="mb-4">
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">URL</label>
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">
URL
</label>
<div className="mt-1">
<div className="flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
@ -230,8 +255,11 @@ export default function EventType(props) {
</div>
</div>
<div className="mb-4">
<label htmlFor="location" className="block text-sm font-medium text-gray-700">Location</label>
{locations.length === 0 && <div className="mt-1 mb-2">
<label htmlFor="location" className="block text-sm font-medium text-gray-700">
Location
</label>
{locations.length === 0 && (
<div className="mt-1 mb-2">
<div className="flex rounded-md shadow-sm">
<Select
name="location"
@ -242,8 +270,10 @@ export default function EventType(props) {
onChange={(e) => openLocationModal(e.value)}
/>
</div>
</div>}
{locations.length > 0 && <ul className="w-96 mt-1">
</div>
)}
{locations.length > 0 && (
<ul className="w-96 mt-1">
{locations.map((location) => (
<li key={location.type} className="bg-blue-50 mb-2 p-2 border">
<div className="flex justify-between">
@ -261,12 +291,73 @@ export default function EventType(props) {
)}
{location.type === LocationType.GoogleMeet && (
<div className="flex-grow flex">
<svg className="h-6 w-6" stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><title></title><path d="M12 0C6.28 0 1.636 4.641 1.636 10.364c0 5.421 4.945 9.817 10.364 9.817V24c6.295-3.194 10.364-8.333 10.364-13.636C22.364 4.64 17.72 0 12 0zM7.5 6.272h6.817a1.363 1.363 0 0 1 1.365 1.365v1.704l2.728-2.727v7.501l-2.726-2.726v1.703a1.362 1.362 0 0 1-1.365 1.365H7.5c-.35 0-.698-.133-.965-.4a1.358 1.358 0 0 1-.4-.965V7.637A1.362 1.362 0 0 1 7.5 6.272Z"></path></svg>
<svg
className="h-6 w-6"
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
role="img"
viewBox="0 0 24 24"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg">
<title></title>
<path d="M12 0C6.28 0 1.636 4.641 1.636 10.364c0 5.421 4.945 9.817 10.364 9.817V24c6.295-3.194 10.364-8.333 10.364-13.636C22.364 4.64 17.72 0 12 0zM7.5 6.272h6.817a1.363 1.363 0 0 1 1.365 1.365v1.704l2.728-2.727v7.501l-2.726-2.726v1.703a1.362 1.362 0 0 1-1.365 1.365H7.5c-.35 0-.698-.133-.965-.4a1.358 1.358 0 0 1-.4-.965V7.637A1.362 1.362 0 0 1 7.5 6.272Z"></path>
</svg>
<span className="ml-2 text-sm">Google Meet</span>
</div>
)}
{location.type === LocationType.Zoom && (
<div className="flex-grow flex">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1329.08 1329.08"
height="1.25em"
width="1.25em"
shapeRendering="geometricPrecision"
textRendering="geometricPrecision"
imageRendering="optimizeQuality"
fillRule="evenodd"
clipRule="evenodd">
<g id="Layer_x0020_1">
<g id="_2116467169744">
<path
d="M664.54 0c367.02 0 664.54 297.52 664.54 664.54s-297.52 664.54-664.54 664.54S0 1031.56 0 664.54 297.52 0 664.54 0z"
fill="#e5e5e4"
fillRule="nonzero"
/>
<path
style={{
fill: "#fff",
fillRule: "nonzero",
}}
d="M664.54 12.94c359.87 0 651.6 291.73 651.6 651.6s-291.73 651.6-651.6 651.6-651.6-291.73-651.6-651.6 291.74-651.6 651.6-651.6z"
/>
<path
d="M664.54 65.21c331 0 599.33 268.33 599.33 599.33 0 331-268.33 599.33-599.33 599.33-331 0-599.33-268.33-599.33-599.33 0-331 268.33-599.33 599.33-599.33z"
fill="#4a8cff"
fillRule="nonzero"
/>
<path
style={{
fill: "#fff",
fillRule: "nonzero",
}}
d="M273.53 476.77v281.65c.25 63.69 52.27 114.95 115.71 114.69h410.55c11.67 0 21.06-9.39 21.06-20.81V570.65c-.25-63.69-52.27-114.95-115.7-114.69H294.6c-11.67 0-21.06 9.39-21.06 20.81zm573.45 109.87l169.5-123.82c14.72-12.18 26.13-9.14 26.13 12.94v377.56c0 25.12-13.96 22.08-26.13 12.94l-169.5-123.57V586.64z"
/>
</g>
</g>
</svg>
<span className="ml-2 text-sm">Zoom Video</span>
</div>
)}
<div className="flex">
<button type="button" onClick={() => openLocationModal(location.type)} className="mr-2 text-sm text-blue-600">Edit</button>
<button
type="button"
onClick={() => openLocationModal(location.type)}
className="mr-2 text-sm text-blue-600">
Edit
</button>
<button onClick={() => removeLocation(location)}>
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " />
</button>
@ -274,37 +365,74 @@ export default function EventType(props) {
</div>
</li>
))}
{locations.length > 0 && locations.length !== locationOptions.length && <li>
<button type="button" className="sm:flex sm:items-start text-sm text-blue-600" onClick={() => setShowLocationModal(true)}>
{locations.length > 0 && locations.length !== locationOptions.length && (
<li>
<button
type="button"
className="sm:flex sm:items-start text-sm text-blue-600"
onClick={() => setShowLocationModal(true)}>
<PlusCircleIcon className="h-6 w-6" />
<span className="ml-1">Add another location option</span>
</button>
</li>}
</ul>}
</li>
)}
</ul>
)}
</div>
<div className="mb-4">
<label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<div className="mt-1">
<textarea ref={descriptionRef} name="description" id="description" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="A quick video meeting." defaultValue={props.eventType.description}></textarea>
<textarea
ref={descriptionRef}
name="description"
id="description"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="A quick video meeting."
defaultValue={props.eventType.description}></textarea>
</div>
</div>
<div className="mb-4">
<label htmlFor="length" className="block text-sm font-medium text-gray-700">Length</label>
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
Length
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input ref={lengthRef} type="number" name="length" id="length" required className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md" placeholder="15" defaultValue={props.eventType.length} />
<input
ref={lengthRef}
type="number"
name="length"
id="length"
required
className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md"
placeholder="15"
defaultValue={props.eventType.length}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
minutes
</div>
</div>
</div>
<div className="mb-4">
<label htmlFor="eventName" className="block text-sm font-medium text-gray-700">Calendar entry name</label>
<label htmlFor="eventName" className="block text-sm font-medium text-gray-700">
Calendar entry name
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input ref={eventNameRef} type="text" name="title" id="title" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Meeting with {USER}" defaultValue={props.eventType.eventName} />
<input
ref={eventNameRef}
type="text"
name="title"
id="title"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Meeting with {USER}"
defaultValue={props.eventType.eventName}
/>
</div>
</div>
<div className="mb-4">
<label htmlFor="additionalFields" className="block text-sm font-medium text-gray-700">Additional Inputs</label>
<label htmlFor="additionalFields" className="block text-sm font-medium text-gray-700">
Additional Inputs
</label>
<ul className="w-96 mt-1">
{customInputs.map((customInput) => (
<li key={customInput.label} className="bg-blue-50 mb-2 p-2 border">
@ -317,12 +445,17 @@ export default function EventType(props) {
<span className="ml-2 text-sm">Type: {customInput.type}</span>
</div>
<div>
<span
className="ml-2 text-sm">{customInput.required ? "Required" : "Optional"}</span>
<span className="ml-2 text-sm">
{customInput.required ? "Required" : "Optional"}
</span>
</div>
</div>
<div className="flex">
<button type="button" onClick={() => openEditCustomModel(customInput)} className="mr-2 text-sm text-blue-600">Edit
<button
type="button"
onClick={() => openEditCustomModel(customInput)}
className="mr-2 text-sm text-blue-600">
Edit
</button>
<button onClick={(e) => removeCustom(customInput, e)}>
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " />
@ -332,7 +465,10 @@ export default function EventType(props) {
</li>
))}
<li>
<button type="button" className="sm:flex sm:items-start text-sm text-blue-600" onClick={() => setShowAddCustomModal(true)}>
<button
type="button"
className="sm:flex sm:items-start text-sm text-blue-600"
onClick={() => setShowAddCustomModal(true)}>
<PlusCircleIcon className="h-6 w-6" />
<span className="ml-1">Add another input</span>
</button>
@ -355,12 +491,18 @@ export default function EventType(props) {
<label htmlFor="ishidden" className="font-medium text-gray-700">
Hide this event type
</label>
<p className="text-gray-500">Hide the event type from your page, so it can only be booked through it's URL.</p>
<p className="text-gray-500">
Hide the event type from your page, so it can only be booked through its URL.
</p>
</div>
</div>
</div>
<button type="submit" className="btn btn-primary">Update</button>
<Link href="/availability"><a className="ml-2 btn btn-white">Cancel</a></Link>
<button type="submit" className="btn btn-primary">
Update
</button>
<Link href="/availability">
<a className="ml-2 btn btn-white">Cancel</a>
</Link>
</form>
</div>
</div>
@ -368,16 +510,15 @@ export default function EventType(props) {
<div>
<div className="bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg mb-2 leading-6 font-medium text-gray-900">
Delete this event type
</h3>
<h3 className="text-lg mb-2 leading-6 font-medium text-gray-900">Delete this event type</h3>
<div className="mb-4 max-w-xl text-sm text-gray-500">
<p>
Once you delete this event type, it will be permanently removed.
</p>
<p>Once you delete this event type, it will be permanently removed.</p>
</div>
<div>
<button onClick={deleteEventTypeHandler} type="button" className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm">
<button
onClick={deleteEventTypeHandler}
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm">
Delete event type
</button>
</div>
@ -385,12 +526,20 @@ export default function EventType(props) {
</div>
</div>
</div>
{showLocationModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
{showLocationModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
@ -398,7 +547,9 @@ export default function EventType(props) {
<LocationMarkerIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">Edit location</h3>
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Edit location
</h3>
</div>
</div>
<form onSubmit={updateLocations}>
@ -423,13 +574,22 @@ export default function EventType(props) {
</div>
</div>
</div>
}
{showAddCustomModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
)}
{showAddCustomModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"/>
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"
/>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
@ -437,7 +597,9 @@ export default function EventType(props) {
<PlusIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">Add new custom input field</h3>
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Add new custom input field
</h3>
<div>
<p className="text-sm text-gray-400">
This input will be shown when booking this event
@ -447,7 +609,9 @@ export default function EventType(props) {
</div>
<form onSubmit={updateCustom}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">Input type</label>
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
Input type
</label>
<Select
name="type"
defaultValue={selectedInputOption}
@ -459,15 +623,28 @@ export default function EventType(props) {
/>
</div>
<div className="mb-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">Label</label>
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
Label
</label>
<div className="mt-1">
<input type="text" name="label" id="label" required
<input
type="text"
name="label"
id="label"
required
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
defaultValue={selectedCustomInput?.label}/>
defaultValue={selectedCustomInput?.label}
/>
</div>
</div>
<div className="flex items-center h-5">
<input id="required" name="required" type="checkbox" className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2" defaultChecked={selectedCustomInput?.required ?? true}/>
<input
id="required"
name="required"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2"
defaultChecked={selectedCustomInput?.required ?? true}
/>
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
Is required
</label>
@ -487,7 +664,7 @@ export default function EventType(props) {
</div>
</div>
</div>
}
)}
</Shell>
</div>
);
@ -499,23 +676,24 @@ const validJson = (jsonString: string) => {
if (o && typeof o === "object") {
return o;
}
} catch (e) {
console.log("Invalid JSON:", e);
}
catch (e) {}
return false;
}
};
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
username: true
}
username: true,
},
});
const credentials = await prisma.credential.findMany({
@ -525,37 +703,45 @@ export async function getServerSideProps(context) {
select: {
id: true,
type: true,
key: true
}
key: true,
},
});
const integrations = [ {
const integrations = [
{
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
enabled: credentials.find((integration) => integration.type === "google_calendar") != null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.png",
description: "For personal and business accounts",
}, {
},
{
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
enabled: credentials.find((integration) => integration.type === "office365_calendar") != null,
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/office-365.png",
description: "For personal and business accounts",
} ];
let locationOptions: OptionBase[] = [
{ value: LocationType.InPerson, label: 'In-person meeting' },
{ value: LocationType.Phone, label: 'Phone call', },
},
];
const hasGoogleCalendarIntegration = integrations.find((i) => i.type === "google_calendar" && i.installed === true && i.enabled)
const locationOptions: OptionBase[] = [
{ value: LocationType.InPerson, label: "In-person meeting" },
{ value: LocationType.Phone, label: "Phone call" },
{ value: LocationType.Zoom, label: "Zoom Video" },
];
const hasGoogleCalendarIntegration = integrations.find(
(i) => i.type === "google_calendar" && i.installed === true && i.enabled
);
if (hasGoogleCalendarIntegration) {
locationOptions.push( { value: LocationType.GoogleMeet, label: 'Google Meet' })
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
}
const hasOfficeIntegration = integrations.find((i) => i.type === "office365_calendar" && i.installed === true && i.enabled)
const hasOfficeIntegration = integrations.find(
(i) => i.type === "office365_calendar" && i.installed === true && i.enabled
);
if (hasOfficeIntegration) {
// TODO: Add default meeting option of the office integration.
// Assuming it's Microsoft Teams.
@ -574,15 +760,15 @@ export async function getServerSideProps(context) {
hidden: true,
locations: true,
eventName: true,
customInputs: true
}
customInputs: true,
},
});
return {
props: {
user,
eventType,
locationOptions
locationOptions,
},
}
};
}