Daily video calls (#542)

* ⬆️ Bump tailwindcss from 2.2.14 to 2.2.15

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.14 to 2.2.15.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* daily table

* rebasing updates

* updating Daily references to a new table

* updating internal notes

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* changing calendso video powered by Daily.co to cal video powered by Daily.co

* updating some of the internal Daily notes

* added a modal for when the call/ link is invalid

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* fixing spacing on the cancel booking modal and adding Daily references in the event manager

* formatting fixes

* updating the readme file

* adding a daily interface in the event manager

* adding daily to the location labels

* added a note to cal event parser

* resolving yarn merge conflicts

* updating dailyReturn to DailyReturnType

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updating read me formatting

* indent space for Daily ReadMe section

* resolving the merge conflicts in the yarn file

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lola-Ojabowale <lola.ojabowale@gmail.com>
This commit is contained in:
Lola 2021-10-07 12:12:39 -04:00 committed by GitHub
parent 58de920951
commit adee3fd211
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 761 additions and 41 deletions

View File

@ -31,6 +31,9 @@ MS_GRAPH_CLIENT_SECRET=
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
#Used for the Daily integration
DAILY_API_KEY=
# E-mail settings
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -283,6 +283,13 @@ Contributions are what make the open source community such an amazing place to b
12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
## Obtaining Daily API Credentials
1. Open [Daily](https://www.daily.co/) and sign into your account.
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
<!-- LICENSE -->
## License

View File

@ -71,6 +71,7 @@ const BookingPage = (props: BookingPageProps) => {
[LocationType.Phone]: "Phone call",
[LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video",
[LocationType.Daily]: "Daily.co Video",
};
const _bookingHandler = (event) => {

View File

@ -80,13 +80,17 @@ export default class CalEventParser {
/**
* Conditionally returns the event's location. When VideoCallData is set,
* it returns the meeting url. Otherwise, the regular location is returned.
*
* For Daily video calls returns the direct link
* @protected
*/
protected getLocation(): string | undefined {
const isDaily = this.calEvent.location === "integrations:daily";
if (this.optionalVideoCallData) {
return this.optionalVideoCallData.url;
}
if (isDaily) {
return process.env.BASE_URL + "/call/" + this.getUid();
}
return this.calEvent.location;
}

239
lib/dailyVideoClient.ts Normal file
View File

@ -0,0 +1,239 @@
import { Credential } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import CalEventParser from "@lib/CalEventParser";
import { AdditionInformation, EntryPoint } from "@lib/emails/EventMail";
import { getIntegrationName } from "@lib/emails/helpers";
import { EventResult } from "@lib/events/EventManager";
import logger from "@lib/logger";
import { CalendarEvent } from "./calendarClient";
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
const log = logger.getChildLogger({ prefix: ["[lib] dailyVideoClient"] });
const translator = short();
export interface DailyVideoCallData {
type: string;
id: string;
password: string;
url: string;
}
function handleErrorsJson(response) {
if (!response.ok) {
response.json().then(console.log);
throw Error(response.statusText);
}
return response.json();
}
const dailyCredential = process.env.DAILY_API_KEY;
interface DailyVideoApiAdapter {
dailyCreateMeeting(event: CalendarEvent): Promise<any>;
dailyUpdateMeeting(uid: string, event: CalendarEvent);
dailyDeleteMeeting(uid: string): Promise<unknown>;
getAvailability(dateFrom, dateTo): Promise<any>;
}
const DailyVideo = (credential): DailyVideoApiAdapter => {
const translateEvent = (event: CalendarEvent) => {
// Documentation at: https://docs.daily.co/reference#list-rooms
// added a 1 hour buffer for room expiration and room entry
const exp = Math.round(new Date(event.endTime).getTime() / 1000) + 60 * 60;
const nbf = Math.round(new Date(event.startTime).getTime() / 1000) - 60 * 60;
return {
privacy: "private",
properties: {
enable_new_call_ui: true,
enable_prejoin_ui: true,
enable_knocking: true,
enable_screenshare: true,
enable_chat: true,
exp: exp,
nbf: nbf,
},
};
};
return {
getAvailability: () => {
return credential;
},
dailyCreateMeeting: (event: CalendarEvent) =>
fetch("https://api.daily.co/v1/rooms", {
method: "POST",
headers: {
Authorization: "Bearer " + dailyCredential,
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
}).then(handleErrorsJson),
dailyDeleteMeeting: (uid: string) =>
fetch("https://api.daily.co/v1/rooms/" + uid, {
method: "DELETE",
headers: {
Authorization: "Bearer " + dailyCredential,
},
}).then(handleErrorsJson),
dailyUpdateMeeting: (uid: string, event: CalendarEvent) =>
fetch("https://api.daily.co/v1/rooms/" + uid, {
method: "POST",
headers: {
Authorization: "Bearer " + dailyCredential,
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
}).then(handleErrorsJson),
};
};
// factory
const videoIntegrations = (withCredentials): DailyVideoApiAdapter[] =>
withCredentials
.map((cred) => {
return DailyVideo(cred);
})
.filter(Boolean);
const getBusyVideoTimes: (withCredentials) => Promise<unknown[]> = (withCredentials) =>
Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) =>
results.reduce((acc, availability) => acc.concat(availability), [])
);
const dailyCreateMeeting = async (
credential: Credential,
calEvent: CalendarEvent,
maybeUid: string = null
): Promise<EventResult> => {
const parser: CalEventParser = new CalEventParser(calEvent, maybeUid);
const uid: string = parser.getUid();
if (!credential) {
throw new Error(
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
);
}
let success = true;
const creationResult = await videoIntegrations([credential])[0]
.dailyCreateMeeting(calEvent)
.catch((e) => {
log.error("createMeeting failed", e, calEvent);
success = false;
});
const currentRoute = process.env.BASE_URL;
const videoCallData: DailyVideoCallData = {
type: "Daily.co Video",
id: creationResult.name,
password: creationResult.password,
url: currentRoute + "/call/" + uid,
};
const entryPoint: EntryPoint = {
entryPointType: getIntegrationName(videoCallData),
uri: videoCallData.url,
label: "Enter Meeting",
pin: "",
};
const additionInformation: AdditionInformation = {
entryPoints: [entryPoint],
};
const organizerMail = new VideoEventOrganizerMail(calEvent, uid, videoCallData, additionInformation);
const attendeeMail = new VideoEventAttendeeMail(calEvent, uid, videoCallData, additionInformation);
try {
await organizerMail.sendEmail();
} catch (e) {
console.error("organizerMail.sendEmail failed", e);
}
if (!creationResult || !creationResult.disableConfirmationEmail) {
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
return {
type: "daily",
success,
uid,
createdEvent: creationResult,
originalEvent: calEvent,
};
};
const dailyUpdateMeeting = async (
credential: Credential,
uidToUpdate: string,
calEvent: CalendarEvent
): Promise<EventResult> => {
const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
if (!credential) {
throw new Error(
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
);
}
let success = true;
const updateResult = credential
? await videoIntegrations([credential])[0]
.dailyUpdateMeeting(uidToUpdate, calEvent)
.catch((e) => {
log.error("updateMeeting failed", e, calEvent);
success = false;
})
: null;
const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid);
const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid);
try {
await organizerMail.sendEmail();
} catch (e) {
console.error("organizerMail.sendEmail failed", e);
}
if (!updateResult || !updateResult.disableConfirmationEmail) {
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
return {
type: credential.type,
success,
uid: newUid,
updatedEvent: updateResult,
originalEvent: calEvent,
};
};
const dailyDeleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
if (credential) {
return videoIntegrations([credential])[0].dailyDeleteMeeting(uid);
}
return Promise.resolve({});
};
export { getBusyVideoTimes, dailyCreateMeeting, dailyUpdateMeeting, dailyDeleteMeeting };

View File

@ -25,11 +25,21 @@ export default class VideoEventAttendeeMail extends EventAttendeeMail {
* @protected
*/
protected getAdditionalBody(): string {
return `
const meetingPassword = this.videoCallData.password;
const meetingId = getFormattedMeetingId(this.videoCallData);
if (meetingId && meetingPassword) {
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br />
<strong>Meeting Password:</strong> ${this.videoCallData.password}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}
}

View File

@ -26,11 +26,19 @@ export default class VideoEventOrganizerMail extends EventOrganizerMail {
* @protected
*/
protected getAdditionalBody(): string {
const meetingPassword = this.videoCallData.password;
const meetingId = getFormattedMeetingId(this.videoCallData);
// This odd indentation is necessary because otherwise the leading tabs will be applied into the event description.
return `
if (meetingPassword && meetingId) {
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br />
<strong>Meeting Password:</strong> ${this.videoCallData.password}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}

View File

@ -4,6 +4,7 @@ import merge from "lodash.merge";
import { v5 as uuidv5 } from "uuid";
import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
import { dailyCreateMeeting, dailyUpdateMeeting } from "@lib/dailyVideoClient";
import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
import { LocationType } from "@lib/location";
@ -43,6 +44,9 @@ interface GetLocationRequestFromIntegrationRequest {
location: string;
}
//const to idenfity a daily event location
const dailyLocation = "integrations:daily";
export default class EventManager {
calendarCredentials: Array<Credential>;
videoCredentials: Array<Credential>;
@ -55,6 +59,19 @@ export default class EventManager {
constructor(credentials: Array<Credential>) {
this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar"));
this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
//for Daily.co video, temporarily pushes a credential for the daily-video-client
const hasDailyIntegration = process.env.DAILY_API_KEY;
const dailyCredential: Credential = {
id: +new Date().getTime(),
type: "daily_video",
key: { apikey: process.env.DAILY_API_KEY },
userId: +new Date().getTime(),
};
if (hasDailyIntegration) {
this.videoCredentials.push(dailyCredential);
}
}
/**
@ -91,13 +108,25 @@ export default class EventManager {
);
const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
return {
type: result.type,
uid: result.createdEvent.id.toString(),
meetingId: result.videoCallData?.id.toString(),
meetingPassword: result.videoCallData?.password,
meetingUrl: result.videoCallData?.url,
};
const isDailyResult = result.type === "daily";
if (isDailyResult) {
return {
type: result.type,
uid: result.createdEvent.name.toString(),
meetingId: result.videoCallData?.id.toString(),
meetingPassword: result.videoCallData?.password,
meetingUrl: result.videoCallData?.url,
};
}
if (!isDailyResult) {
return {
type: result.type,
uid: result.createdEvent.id.toString(),
meetingId: result.videoCallData?.id.toString(),
meetingPassword: result.videoCallData?.password,
meetingUrl: result.videoCallData?.url,
};
}
});
return {
@ -136,7 +165,8 @@ export default class EventManager {
},
});
const isDedicated = EventManager.isDedicatedIntegration(event.location);
const isDedicated =
EventManager.isDedicatedIntegration(event.location) || event.location === dailyLocation;
let results: Array<EventResult> = [];
let optionalVideoCallData: VideoCallData | undefined = undefined;
@ -198,6 +228,7 @@ export default class EventManager {
* @param optionalVideoCallData
* @private
*/
private createAllCalendarEvents(
event: CalendarEvent,
noMail: boolean,
@ -215,8 +246,10 @@ export default class EventManager {
* @param event
* @private
*/
private getVideoCredential(event: CalendarEvent): Credential | undefined {
const integrationName = event.location.replace("integrations:", "");
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
}
@ -232,8 +265,12 @@ export default class EventManager {
private createVideoEvent(event: CalendarEvent, maybeUid?: string): Promise<EventResult> {
const credential = this.getVideoCredential(event);
if (credential) {
const isDaily = event.location === dailyLocation;
if (credential && !isDaily) {
return createMeeting(credential, event, maybeUid);
} else if (isDaily) {
return dailyCreateMeeting(credential, event, maybeUid);
} else {
return Promise.reject("No suitable credentials given for the requested integration name.");
}
@ -271,8 +308,9 @@ export default class EventManager {
*/
private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
const credential = this.getVideoCredential(event);
const isDaily = event.location === dailyLocation;
if (credential) {
if (credential && !isDaily) {
const bookingRef = booking.references.filter((ref) => ref.type === credential.type)[0];
return updateMeeting(credential, bookingRef.uid, event).then((returnVal: EventResult) => {
@ -283,6 +321,10 @@ export default class EventManager {
return returnVal;
});
} else {
if (isDaily) {
const bookingRefUid = booking.references.filter((ref) => ref.type === "daily")[0].uid;
return dailyUpdateMeeting(credential, bookingRefUid, event);
}
return Promise.reject("No suitable credentials given for the requested integration name.");
}
}
@ -300,7 +342,8 @@ export default class EventManager {
*/
private static isDedicatedIntegration(location: string): boolean {
// Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
return location === "integrations:zoom";
return location === "integrations:zoom" || location === dailyLocation;
}
/**
@ -313,7 +356,11 @@ export default class EventManager {
private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) {
const location = locationObj.location;
if (location === LocationType.GoogleMeet.valueOf() || location === LocationType.Zoom.valueOf()) {
if (
location === LocationType.GoogleMeet.valueOf() ||
location === LocationType.Zoom.valueOf() ||
location === LocationType.Daily.valueOf()
) {
const requestId = uuidv5(location, uuidv5.URL);
return {

View File

@ -12,8 +12,8 @@ export function getIntegrationName(name: string) {
return "Stripe";
case "apple_calendar":
return "Apple Calendar";
default:
return "Unknown";
case "daily_video":
return "Daily";
}
}

View File

@ -3,4 +3,5 @@ export enum LocationType {
Phone = "phone",
GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom",
Daily = "integrations:daily",
}

View File

@ -28,6 +28,7 @@
},
"dependencies": {
"@headlessui/react": "^1.4.1",
"@daily-co/daily-js": "^0.16.0",
"@heroicons/react": "^1.0.4",
"@hookform/resolvers": "^2.8.1",
"@jitsu/sdk-js": "^2.2.4",
@ -84,6 +85,7 @@
"react-select": "^4.3.1",
"react-timezone-select": "^1.0.7",
"react-use-intercom": "1.4.0",
"react-router-dom": "^5.2.0",
"short-uuid": "^4.2.0",
"stripe": "^8.168.0",
"superjson": "1.7.5",

View File

@ -22,6 +22,13 @@ import { getBusyVideoTimes } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
export interface DailyReturnType {
name: string;
url: string;
id: string;
created_at: string;
}
dayjs.extend(dayjsBusinessDays);
dayjs.extend(utc);
dayjs.extend(isBetween);
@ -249,7 +256,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const attendeesList = [...invitee, ...guests, ...teamMembers];
const seed = `${users[0].username}:${dayjs(reqBody.start).utc().format()}`;
const seed = `${users[0].username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const evt: CalendarEvent = {
@ -353,8 +360,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
selectedCalendars
);
const videoBusyTimes = await getBusyVideoTimes(credentials);
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter((time) => time);
calendarBusyTimes.push(...videoBusyTimes);
console.log("calendarBusyTimes==>>>", calendarBusyTimes);
const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
@ -445,6 +453,46 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
//for Daily.co video calls will grab the meeting token for the call
const isDaily = evt.location === "integrations:daily";
let dailyEvent: DailyReturnType;
if (!rescheduleUid) {
dailyEvent = results.filter((ref) => ref.type === "daily")[0]?.createdEvent as DailyReturnType;
} else {
dailyEvent = results.filter((ref) => ref.type === "daily_video")[0]?.updatedEvent as DailyReturnType;
}
let meetingToken;
if (isDaily) {
const response = await fetch("https://api.daily.co/v1/meeting-tokens", {
method: "POST",
body: JSON.stringify({ properties: { room_name: dailyEvent.name, is_owner: true } }),
headers: {
Authorization: "Bearer " + process.env.DAILY_API_KEY,
"Content-Type": "application/json",
},
});
meetingToken = await response.json();
}
//for Daily.co video calls will update the dailyEventReference table
if (isDaily) {
await prisma.dailyEventReference.create({
data: {
dailyurl: dailyEvent.url,
dailytoken: meetingToken.token,
booking: {
connect: {
uid: booking.uid,
},
},
},
});
}
if (eventType.requiresConfirmation && !rescheduleUid) {
await new EventOrganizerRequestMail(evt, uid).sendEmail();
}

View File

@ -11,6 +11,8 @@ import { deleteMeeting } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
import { dailyDeleteMeeting } from "../../lib/dailyVideoClient";
export default async function handler(req, res) {
// just bail if it not a DELETE
if (req.method !== "DELETE" && req.method !== "POST") {
@ -37,6 +39,7 @@ export default async function handler(req, res) {
},
},
attendees: true,
location: true,
references: {
select: {
uid: true,
@ -118,6 +121,13 @@ export default async function handler(req, res) {
return await deleteMeeting(credential, bookingRefUid);
}
}
//deleting a Daily meeting
const isDaily = bookingToDelete.location === "integrations:daily";
const bookingUID = bookingToDelete.references.filter((ref) => ref.type === "daily")[0]?.uid;
if (isDaily) {
return await dailyDeleteMeeting(credential, bookingUID);
}
});
if (bookingToDelete && bookingToDelete.paid) {

90
pages/call/[uid].tsx Normal file
View File

@ -0,0 +1,90 @@
import DailyIframe from "@daily-co/daily-js";
import { getSession } from "next-auth/client";
import { useRouter } from "next/router";
import { useEffect } from "react";
import prisma from "../../lib/prisma";
export default function JoinCall(props, session) {
const router = useRouter();
//if no booking redirectis to the 404 page
const emptyBooking = props.booking === null;
useEffect(() => {
if (emptyBooking) {
router.push("/call/no-meeting-found");
}
});
useEffect(() => {
if (!emptyBooking && session.userid !== props.booking.user.id) {
const callFrame = DailyIframe.createFrame({
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
});
callFrame.join({
url: props.booking.dailyRef.dailyurl,
showLeaveButton: true,
});
}
if (!emptyBooking && session.userid === props.booking.user.id) {
const callFrame = DailyIframe.createFrame({
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
});
callFrame.join({
url: props.booking.dailyRef.dailyurl,
showLeaveButton: true,
token: props.booking.dailyRef.dailytoken,
});
}
}, []);
return JoinCall;
}
export async function getServerSideProps(context) {
const booking = await prisma.booking.findFirst({
where: {
uid: context.query.uid,
},
select: {
id: true,
user: {
select: {
credentials: true,
},
},
attendees: true,
dailyRef: {
select: {
dailyurl: true,
dailytoken: true,
},
},
references: {
select: {
uid: true,
type: true,
},
},
},
});
const session = await getSession();
return {
props: {
booking: booking,
session: session,
},
};
}

View File

@ -0,0 +1,52 @@
import { XIcon } from "@heroicons/react/outline";
import { ArrowRightIcon } from "@heroicons/react/solid";
import { HeadSeo } from "@components/seo/head-seo";
import Button from "@components/ui/Button";
export default function NoMeetingFound() {
return (
<div>
<HeadSeo title={`No meeting Found`} description={`No Meeting Found`} />
<main className="max-w-3xl mx-auto my-24">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div
className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
<XIcon className="w-6 h-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
No Meeting Found
</h3>
</div>
<div className="mt-2">
<p className="text-sm text-center text-gray-500">
This meeting does not exist. Contact the meeting owner for an updated link.
</p>
</div>
</div>
<div className="mt-5 text-center sm:mt-6">
<div className="mt-5">
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRightIcon}>
Go back home
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "DailyEventReference" (
"id" SERIAL NOT NULL,
"dailyurl" TEXT NOT NULL DEFAULT E'dailycallurl',
"dailytoken" TEXT NOT NULL DEFAULT E'dailytoken',
"bookingId" INTEGER,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DailyEventReference_bookingId_unique" ON "DailyEventReference"("bookingId");
-- AddForeignKey
ALTER TABLE "DailyEventReference" ADD FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -162,6 +162,14 @@ enum BookingStatus {
PENDING @map("pending")
}
model DailyEventReference {
id Int @id @default(autoincrement())
dailyurl String @default("dailycallurl")
dailytoken String @default("dailytoken")
booking Booking? @relation(fields: [bookingId], references: [id])
bookingId Int?
}
model Booking {
id Int @id @default(autoincrement())
uid String @unique
@ -179,6 +187,8 @@ model Booking {
attendees Attendee[]
location String?
dailyRef DailyEventReference?
createdAt DateTime @default(now())
updatedAt DateTime?
confirmed Boolean @default(true)

123
yarn.lock
View File

@ -282,15 +282,17 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.12.13", "@babel/template@^7.15.4", "@babel/template@^7.3.3":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==
dependencies:
"@babel/code-frame" "^7.14.5"
"@babel/parser" "^7.15.4"
@ -391,6 +393,17 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@daily-co/daily-js@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.16.0.tgz#9020104bb88de62dcc1966e713da65844243b9ab"
integrity sha512-DBWzbZs2IR7uYqfbABva1Ms3f/oX85dnQnCpVbGbexTN63LPIGknFSQp31ZYED88qcG+YJNydywBTb+ApNiNXA==
dependencies:
"@babel/runtime" "^7.12.5"
bowser "^2.8.1"
events "^3.1.0"
fast-equals "^1.6.3"
lodash "^4.17.15"
"@emotion/cache@^11.4.0":
version "11.4.0"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz"
@ -2478,6 +2491,11 @@ bn.js@^5.0.0, bn.js@^5.1.1:
version "5.2.0"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz"
bowser@^2.8.1:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
@ -3703,7 +3721,7 @@ eventemitter2@^6.4.3:
version "6.4.4"
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz"
events@^3.0.0:
events@^3.0.0, events@^3.1.0:
version "3.3.0"
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
@ -3811,6 +3829,11 @@ fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
fast-equals@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
fast-glob@^3.1.1, fast-glob@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz"
@ -4359,6 +4382,18 @@ highlight.js@^10.7.1:
version "10.7.3"
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz"
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz"
@ -4367,7 +4402,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
dependencies:
@ -4826,6 +4861,11 @@ is-windows@^1.0.2:
resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
@ -5795,9 +5835,10 @@ lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz"
lodash@4.17.21, lodash@4.x, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
lodash@4.17.21, lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.0.0, log-symbols@^4.1.0:
version "4.1.0"
@ -5815,7 +5856,7 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
dependencies:
@ -5949,6 +5990,14 @@ min-document@^2.19.0:
dependencies:
dom-walk "^0.1.0"
mini-create-react-context@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
dependencies:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-svg-data-uri@^1.2.3:
version "1.3.3"
resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz"
@ -6613,6 +6662,13 @@ path-parse@^1.0.6:
version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@ -7140,7 +7196,7 @@ react-is@17.0.2, react-is@^17.0.1:
version "17.0.2"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@ -7188,6 +7244,35 @@ react-remove-scroll@^2.4.0:
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-router-dom@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"
integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.1"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d"
integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-select@^4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz"
@ -7371,6 +7456,11 @@ resolve-from@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve@^1.10.0, resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz"
@ -8063,6 +8153,16 @@ timm@^1.6.1:
resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f"
integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@^1.4.1:
version "1.4.2"
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@ -8430,6 +8530,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
verror@1.10.0:
version "1.10.0"
resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz"