Added zoom as an event location and fixed ESLint
This commit is contained in:
parent
dada6a3a79
commit
bc47975316
3 changed files with 1048 additions and 764 deletions
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {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 {EventTypeCustomInputType} from "../../lib/eventTypeInput";
|
||||
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 { 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 [ loading, setLoading ] = useState(false);
|
||||
const [ error, setError ] = useState(false);
|
||||
const [is24h, setIs24h] = useState(false);
|
||||
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,108 +158,202 @@ 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 && (
|
||||
<div className="mb-4">
|
||||
<span className="block text-sm font-medium text-gray-700">Location</span>
|
||||
{locations.map( (location) => (
|
||||
{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) {
|
||||
|
@ -265,11 +364,11 @@ export async function getServerSideProps(context) {
|
|||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
email:true,
|
||||
email: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
eventTypes: true
|
||||
}
|
||||
eventTypes: true,
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
|
@ -284,25 +383,25 @@ export async function getServerSideProps(context) {
|
|||
length: true,
|
||||
locations: true,
|
||||
customInputs: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let booking = null;
|
||||
|
||||
if(context.query.rescheduleUid) {
|
||||
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,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {EventTypeCustomInput, EventTypeCustomInputType} from "../../../lib/eventTypeInput";
|
||||
import {PlusIcon} from "@heroicons/react/solid";
|
||||
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 [ 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 [, 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 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',
|
||||
body: JSON.stringify({id: props.eventType.id}),
|
||||
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));
|
||||
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;
|
||||
};
|
||||
|
@ -143,10 +155,10 @@ export default function EventType(props) {
|
|||
details = { address: e.target.address.value };
|
||||
}
|
||||
|
||||
const existingIdx = locations.findIndex( (loc) => e.target.location.value === loc.type );
|
||||
const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type);
|
||||
if (existingIdx !== -1) {
|
||||
let copy = locations;
|
||||
copy[ existingIdx ] = { ...locations[ existingIdx ], ...details };
|
||||
const copy = locations;
|
||||
copy[existingIdx] = { ...locations[existingIdx], ...details };
|
||||
setLocations(copy);
|
||||
} else {
|
||||
setLocations(locations.concat({ type: e.target.location.value, ...details }));
|
||||
|
@ -156,7 +168,7 @@ export default function EventType(props) {
|
|||
};
|
||||
|
||||
const removeLocation = (selectedLocation) => {
|
||||
setLocations(locations.filter( (location) => location.type !== selectedLocation.type ));
|
||||
setLocations(locations.filter((location) => location.type !== selectedLocation.type));
|
||||
};
|
||||
|
||||
const updateCustom = (e) => {
|
||||
|
@ -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;
|
||||
|
@ -177,7 +189,7 @@ export default function EventType(props) {
|
|||
input.type = customInput.type;
|
||||
setCustomInputs(customInputs);
|
||||
}
|
||||
} else{
|
||||
} else {
|
||||
setCustomInputs(customInputs.concat(customInput));
|
||||
}
|
||||
closeAddCustomModal();
|
||||
|
@ -185,12 +197,12 @@ export default function EventType(props) {
|
|||
|
||||
const removeCustom = (customInput, e) => {
|
||||
e.preventDefault();
|
||||
const index = customInputs.findIndex(inp => inp.id === customInput.id);
|
||||
if (index >= 0){
|
||||
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,9 +270,11 @@ export default function EventType(props) {
|
|||
onChange={(e) => openLocationModal(e.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
{locations.length > 0 && <ul className="w-96 mt-1">
|
||||
{locations.map( (location) => (
|
||||
</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">
|
||||
{location.type === LocationType.InPerson && (
|
||||
|
@ -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,39 +365,76 @@ 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) => (
|
||||
{customInputs.map((customInput) => (
|
||||
<li key={customInput.label} className="bg-blue-50 mb-2 p-2 border">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
|
@ -317,22 +445,30 @@ 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 "/>
|
||||
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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">​</span>
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</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">​</span>
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</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,21 +623,34 @@ 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>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="id" id="id" value={selectedCustomInput?.id}/>
|
||||
<input type="hidden" name="id" id="id" value={selectedCustomInput?.id} />
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
|
@ -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,
|
||||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue