Resolved conflicts; included reference creation into EventManager; use EventManager everywhere
This commit is contained in:
		
						commit
						86e2add30f
					
				
					 19 changed files with 843 additions and 308 deletions
				
			
		|  | @ -32,3 +32,5 @@ EMAIL_SERVER_PORT=587 | |||
| EMAIL_SERVER_USER='<office365_emailAddress>' | ||||
| # Keep in mind that if you have 2FA enabled, you will need to provision an App Password. | ||||
| EMAIL_SERVER_PASSWORD='<office365_password>' | ||||
| # ApiKey for cronjobs | ||||
| CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0' | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ import Link from "next/link"; | |||
| 
 | ||||
| const PoweredByCalendso = () => ( | ||||
|   <div className="text-xs text-center sm:text-right pt-1"> | ||||
|     <Link href="https://calendso.com"> | ||||
|       <a className="dark:text-white text-gray-500 opacity-50 hover:opacity-100"> | ||||
|     <Link href={`https://calendso.com?utm_source=embed&utm_medium=powered-by-button`}> | ||||
|       <a target="_blank" className="dark:text-white text-gray-500 opacity-50 hover:opacity-100"> | ||||
|         powered by{" "} | ||||
|         <img | ||||
|           style={{ top: -2 }} | ||||
|  |  | |||
|  | @ -574,7 +574,7 @@ const updateEvent = async ( | |||
|   credential: Credential, | ||||
|   uidToUpdate: string, | ||||
|   calEvent: CalendarEvent, | ||||
|   noMail: false | ||||
|   noMail = false | ||||
| ): Promise<EventResult> => { | ||||
|   const parser: CalEventParser = new CalEventParser(calEvent); | ||||
|   const newUid: string = parser.getUid(); | ||||
|  | @ -626,12 +626,4 @@ const deleteEvent = (credential: Credential, uid: string): Promise<unknown> => { | |||
|   return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| export { | ||||
|   getBusyCalendarTimes, | ||||
|   createEvent, | ||||
|   updateEvent, | ||||
|   deleteEvent, | ||||
|   CalendarEvent, | ||||
|   listCalendars, | ||||
|   IntegrationCalendar, | ||||
| }; | ||||
| export { getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, listCalendars }; | ||||
|  |  | |||
|  | @ -46,6 +46,31 @@ export default class EventOrganizerMail extends EventMail { | |||
|     return icsEvent.value; | ||||
|   } | ||||
| 
 | ||||
|   protected getBodyHeader(): string { | ||||
|     return "A new event has been scheduled."; | ||||
|   } | ||||
| 
 | ||||
|   protected getBodyText(): string { | ||||
|     return "You and any other attendees have been emailed with this information."; | ||||
|   } | ||||
| 
 | ||||
|   protected getImage(): string { | ||||
|     return `<svg
 | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       style="height: 60px; width: 60px; color: #31c48d" | ||||
|       fill="none" | ||||
|       viewBox="0 0 24 24" | ||||
|       stroke="currentColor" | ||||
|     > | ||||
|       <path | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         stroke-width="2" | ||||
|         d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" | ||||
|       /> | ||||
|     </svg>`;
 | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the email text as HTML representation. | ||||
|    * | ||||
|  | @ -67,22 +92,9 @@ export default class EventOrganizerMail extends EventMail { | |||
|       margin-top: 40px; | ||||
|     " | ||||
|   > | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       style="height: 60px; width: 60px; color: #31c48d" | ||||
|       fill="none" | ||||
|       viewBox="0 0 24 24" | ||||
|       stroke="currentColor" | ||||
|     > | ||||
|       <path | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         stroke-width="2" | ||||
|         d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" | ||||
|       /> | ||||
|     </svg> | ||||
|     <h1 style="font-weight: 500; color: #161e2e;">A new event has been scheduled.</h1> | ||||
|     <p style="color: #4b5563; margin-bottom: 30px;">You and any other attendees have been emailed with this information.</p> | ||||
|     ${this.getImage()} | ||||
|     <h1 style="font-weight: 500; color: #161e2e;">${this.getBodyHeader()}</h1> | ||||
|     <p style="color: #4b5563; margin-bottom: 30px;">${this.getBodyText()}</p> | ||||
|     <hr /> | ||||
|     <table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;"> | ||||
|       <colgroup> | ||||
|  | @ -165,8 +177,6 @@ export default class EventOrganizerMail extends EventMail { | |||
|    * @protected | ||||
|    */ | ||||
|   protected getNodeMailerPayload(): Record<string, unknown> { | ||||
|     const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); | ||||
| 
 | ||||
|     return { | ||||
|       icalEvent: { | ||||
|         filename: "event.ics", | ||||
|  | @ -174,14 +184,19 @@ export default class EventOrganizerMail extends EventMail { | |||
|       }, | ||||
|       from: `Calendso <${this.getMailerOptions().from}>`, | ||||
|       to: this.calEvent.organizer.email, | ||||
|       subject: `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${ | ||||
|         this.calEvent.type | ||||
|       }`,
 | ||||
|       subject: this.getSubject(), | ||||
|       html: this.getHtmlRepresentation(), | ||||
|       text: this.getPlainTextRepresentation(), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   protected getSubject(): string { | ||||
|     const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); | ||||
|     return `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${ | ||||
|       this.calEvent.type | ||||
|     }`;
 | ||||
|   } | ||||
| 
 | ||||
|   protected printNodeMailerError(error: string): void { | ||||
|     console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); | ||||
|   } | ||||
|  |  | |||
							
								
								
									
										50
									
								
								lib/emails/EventOrganizerRequestMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/emails/EventOrganizerRequestMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import toArray from "dayjs/plugin/toArray"; | ||||
| import localizedFormat from "dayjs/plugin/localizedFormat"; | ||||
| import EventOrganizerMail from "@lib/emails/EventOrganizerMail"; | ||||
| 
 | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| dayjs.extend(toArray); | ||||
| dayjs.extend(localizedFormat); | ||||
| 
 | ||||
| export default class EventOrganizerRequestMail extends EventOrganizerMail { | ||||
|   protected getBodyHeader(): string { | ||||
|     return "A new event is waiting for your approval."; | ||||
|   } | ||||
| 
 | ||||
|   protected getBodyText(): string { | ||||
|     return "Check your bookings page to confirm or reject the booking."; | ||||
|   } | ||||
| 
 | ||||
|   protected getAdditionalBody(): string { | ||||
|     return `<a href="${process.env.BASE_URL}/bookings">Confirm or reject the booking</a>`; | ||||
|   } | ||||
| 
 | ||||
|   protected getImage(): string { | ||||
|     return `<svg
 | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       style="height: 60px; width: 60px; color: #01579b" | ||||
|       fill="none" | ||||
|       viewBox="0 0 24 24" | ||||
|       stroke="currentColor" | ||||
|     > | ||||
|       <path | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         stroke-width="2" | ||||
|         d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" | ||||
|       /> | ||||
|     </svg>`;
 | ||||
|   } | ||||
| 
 | ||||
|   protected getSubject(): string { | ||||
|     const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); | ||||
|     return `New event request: ${this.calEvent.attendees[0].name} - ${organizerStart.format( | ||||
|       "LT dddd, LL" | ||||
|     )} - ${this.calEvent.type}`;
 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								lib/emails/EventOrganizerRequestReminderMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/emails/EventOrganizerRequestReminderMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import toArray from "dayjs/plugin/toArray"; | ||||
| import localizedFormat from "dayjs/plugin/localizedFormat"; | ||||
| import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail"; | ||||
| 
 | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| dayjs.extend(toArray); | ||||
| dayjs.extend(localizedFormat); | ||||
| 
 | ||||
| export default class EventOrganizerRequestReminderMail extends EventOrganizerRequestMail { | ||||
|   protected getBodyHeader(): string { | ||||
|     return "An event is still waiting for your approval."; | ||||
|   } | ||||
| 
 | ||||
|   protected getSubject(): string { | ||||
|     const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); | ||||
|     return `Event request is still waiting: ${this.calEvent.attendees[0].name} - ${organizerStart.format( | ||||
|       "LT dddd, LL" | ||||
|     )} - ${this.calEvent.type}`;
 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										91
									
								
								lib/emails/EventRejectionMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								lib/emails/EventRejectionMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,91 @@ | |||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| import EventMail from "./EventMail"; | ||||
| 
 | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import localizedFormat from "dayjs/plugin/localizedFormat"; | ||||
| 
 | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| dayjs.extend(localizedFormat); | ||||
| 
 | ||||
| export default class EventRejectionMail extends EventMail { | ||||
|   /** | ||||
|    * Returns the email text as HTML representation. | ||||
|    * | ||||
|    * @protected | ||||
|    */ | ||||
|   protected getHtmlRepresentation(): string { | ||||
|     return ( | ||||
|       ` | ||||
| <body style="background: #f4f5f7; font-family: Helvetica, sans-serif"> | ||||
|   <div | ||||
|     style=" | ||||
|       margin: 0 auto; | ||||
|       max-width: 450px; | ||||
|       background: white; | ||||
|       border-radius: 0.75rem; | ||||
|       border: 1px solid #e5e7eb; | ||||
|       padding: 2rem 2rem 2rem 2rem; | ||||
|       text-align: center; | ||||
|       margin-top: 40px; | ||||
|     " | ||||
|   > | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       style="height: 60px; width: 60px; color: #ba2525" | ||||
|       fill="none" | ||||
|       viewBox="0 0 24 24" | ||||
|       stroke="currentColor" | ||||
|     > | ||||
|       <path | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         stroke-width="2" | ||||
|          d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" | ||||
|       /> | ||||
|     </svg> | ||||
|     <h1 style="font-weight: 500; color: #161e2e;">Your meeting request has been rejected</h1> | ||||
|     <p style="color: #4b5563; margin-bottom: 30px;">You and any other attendees have been emailed with this information.</p> | ||||
|     <hr /> | ||||
|     ` +
 | ||||
|       ` | ||||
|   </div> | ||||
|   <div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;"> | ||||
|     <img style="opacity: 0.25; width: 120px;" src="https://app.calendso.com/calendso-logo-word.svg" alt="Calendso Logo"></div> | ||||
| </body> | ||||
|   ` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the payload object for the nodemailer. | ||||
|    * | ||||
|    * @protected | ||||
|    */ | ||||
|   protected getNodeMailerPayload(): Record<string, unknown> { | ||||
|     return { | ||||
|       to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`, | ||||
|       from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, | ||||
|       replyTo: this.calEvent.organizer.email, | ||||
|       subject: `Rejected: ${this.calEvent.type} with ${ | ||||
|         this.calEvent.organizer.name | ||||
|       } on ${this.getInviteeStart().format("dddd, LL")}`,
 | ||||
|       html: this.getHtmlRepresentation(), | ||||
|       text: this.getPlainTextRepresentation(), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   protected printNodeMailerError(error: string): void { | ||||
|     console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the inviteeStart value used at multiple points. | ||||
|    * | ||||
|    * @private | ||||
|    */ | ||||
|   protected getInviteeStart(): Dayjs { | ||||
|     return <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone); | ||||
|   } | ||||
| } | ||||
|  | @ -2,6 +2,7 @@ import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient"; | |||
| import { Credential } from "@prisma/client"; | ||||
| import async from "async"; | ||||
| import { createMeeting, updateMeeting } from "@lib/videoClient"; | ||||
| import prisma from "@lib/prisma"; | ||||
| 
 | ||||
| export interface EventResult { | ||||
|   type: string; | ||||
|  | @ -12,13 +13,18 @@ export interface EventResult { | |||
|   originalEvent: CalendarEvent; | ||||
| } | ||||
| 
 | ||||
| export interface CreateUpdateResult { | ||||
|   results: Array<EventResult>; | ||||
|   referencesToCreate: Array<PartialReference>; | ||||
| } | ||||
| 
 | ||||
| export interface PartialBooking { | ||||
|   id: number; | ||||
|   references: Array<PartialReference>; | ||||
| } | ||||
| 
 | ||||
| export interface PartialReference { | ||||
|   id: number; | ||||
|   id?: number; | ||||
|   type: string; | ||||
|   uid: string; | ||||
| } | ||||
|  | @ -32,7 +38,7 @@ export default class EventManager { | |||
|     this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); | ||||
|   } | ||||
| 
 | ||||
|   public async create(event: CalendarEvent): Promise<Array<EventResult>> { | ||||
|   public async create(event: CalendarEvent): Promise<CreateUpdateResult> { | ||||
|     const isVideo = EventManager.isIntegration(event.location); | ||||
| 
 | ||||
|     // First, create all calendar events. If this is a video event, don't send a mail right here.
 | ||||
|  | @ -43,10 +49,37 @@ export default class EventManager { | |||
|       results.push(await this.createVideoEvent(event)); | ||||
|     } | ||||
| 
 | ||||
|     return results; | ||||
|     const referencesToCreate: Array<PartialReference> = results.map((result) => { | ||||
|       return { | ||||
|         type: result.type, | ||||
|         uid: result.createdEvent.id.toString(), | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     return { | ||||
|       results, | ||||
|       referencesToCreate, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   public async update(event: CalendarEvent, booking: PartialBooking): Promise<Array<EventResult>> { | ||||
|   public async update(event: CalendarEvent, rescheduleUid: string): Promise<CreateUpdateResult> { | ||||
|     // Get details of existing booking.
 | ||||
|     const booking = await prisma.booking.findFirst({ | ||||
|       where: { | ||||
|         uid: rescheduleUid, | ||||
|       }, | ||||
|       select: { | ||||
|         id: true, | ||||
|         references: { | ||||
|           select: { | ||||
|             id: true, | ||||
|             type: true, | ||||
|             uid: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     const isVideo = EventManager.isIntegration(event.location); | ||||
| 
 | ||||
|     // First, update all calendar events. If this is a video event, don't send a mail right here.
 | ||||
|  | @ -57,7 +90,30 @@ export default class EventManager { | |||
|       results.push(await this.updateVideoEvent(event, booking)); | ||||
|     } | ||||
| 
 | ||||
|     return results; | ||||
|     // Now we can delete the old booking and its references.
 | ||||
|     const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ | ||||
|       where: { | ||||
|         bookingId: booking.id, | ||||
|       }, | ||||
|     }); | ||||
|     const attendeeDeletes = prisma.attendee.deleteMany({ | ||||
|       where: { | ||||
|         bookingId: booking.id, | ||||
|       }, | ||||
|     }); | ||||
|     const bookingDeletes = prisma.booking.delete({ | ||||
|       where: { | ||||
|         uid: rescheduleUid, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     // Wait for all deletions to be applied.
 | ||||
|     await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); | ||||
| 
 | ||||
|     return { | ||||
|       results, | ||||
|       referencesToCreate: [...booking.references], | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       description: req.body.description, | ||||
|       length: parseInt(req.body.length), | ||||
|       hidden: req.body.hidden, | ||||
|       requiresConfirmation: req.body.requiresConfirmation, | ||||
|       locations: req.body.locations, | ||||
|       eventName: req.body.eventName, | ||||
|       customInputs: !req.body.customInputs | ||||
|  |  | |||
|  | @ -10,12 +10,14 @@ import { LocationType } from "@lib/location"; | |||
| import merge from "lodash.merge"; | ||||
| import dayjs from "dayjs"; | ||||
| import logger from "../../../lib/logger"; | ||||
| import EventManager, { EventResult } from "@lib/events/EventManager"; | ||||
| import EventManager, { CreateUpdateResult, EventResult } from "@lib/events/EventManager"; | ||||
| import { User } from "@prisma/client"; | ||||
| 
 | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import dayjsBusinessDays from "dayjs-business-days"; | ||||
| import { Exception } from "handlebars"; | ||||
| import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail"; | ||||
| 
 | ||||
| dayjs.extend(dayjsBusinessDays); | ||||
| dayjs.extend(utc); | ||||
|  | @ -116,6 +118,25 @@ const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromI | |||
|   return null; | ||||
| }; | ||||
| 
 | ||||
| export async function handleLegacyConfirmationMail( | ||||
|   results: Array<EventResult>, | ||||
|   selectedEventType: { requiresConfirmation: boolean }, | ||||
|   evt: CalendarEvent, | ||||
|   hashUID: string | ||||
| ): Promise<{ error: Exception; message: string | null }> { | ||||
|   if (results.length === 0 && !selectedEventType.requiresConfirmation) { | ||||
|     // Legacy as well, as soon as we have a separate email integration class. Just used
 | ||||
|     // to send an email even if there is no integration at all.
 | ||||
|     try { | ||||
|       const mail = new EventAttendeeMail(evt, hashUID); | ||||
|       await mail.sendEmail(); | ||||
|     } catch (e) { | ||||
|       return { error: e, message: "Booking failed" }; | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> { | ||||
|   const { user } = req.query; | ||||
|   log.debug(`Booking ${user} started`); | ||||
|  | @ -210,6 +231,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         periodStartDate: true, | ||||
|         periodEndDate: true, | ||||
|         periodCountCalendarDays: true, | ||||
|         requiresConfirmation: true, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|  | @ -303,25 +325,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|     let referencesToCreate = []; | ||||
| 
 | ||||
|     if (rescheduleUid) { | ||||
|       // Reschedule event
 | ||||
|       const booking = await prisma.booking.findFirst({ | ||||
|         where: { | ||||
|           uid: rescheduleUid, | ||||
|         }, | ||||
|         select: { | ||||
|           id: true, | ||||
|           references: { | ||||
|             select: { | ||||
|               id: true, | ||||
|               type: true, | ||||
|               uid: true, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       // Use EventManager to conditionally use all needed integrations.
 | ||||
|       results = await eventManager.update(evt, booking); | ||||
|       const updateResults: CreateUpdateResult = await eventManager.update(evt, rescheduleUid); | ||||
| 
 | ||||
|       if (results.length > 0 && results.every((res) => !res.success)) { | ||||
|         const error = { | ||||
|  | @ -333,30 +338,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         return res.status(500).json(error); | ||||
|       } | ||||
| 
 | ||||
|       // Clone elements
 | ||||
|       referencesToCreate = [...booking.references]; | ||||
| 
 | ||||
|       // Now we can delete the old booking and its references.
 | ||||
|       const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ | ||||
|         where: { | ||||
|           bookingId: booking.id, | ||||
|         }, | ||||
|       }); | ||||
|       const attendeeDeletes = prisma.attendee.deleteMany({ | ||||
|         where: { | ||||
|           bookingId: booking.id, | ||||
|         }, | ||||
|       }); | ||||
|       const bookingDeletes = prisma.booking.delete({ | ||||
|         where: { | ||||
|           uid: rescheduleUid, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); | ||||
|     } else { | ||||
|       // Forward results
 | ||||
|       results = updateResults.results; | ||||
|       referencesToCreate = updateResults.referencesToCreate; | ||||
|     } else if (!selectedEventType.requiresConfirmation) { | ||||
|       // Use EventManager to conditionally use all needed integrations.
 | ||||
|       results = await eventManager.create(evt); | ||||
|       const createResults: CreateUpdateResult = await eventManager.create(evt); | ||||
| 
 | ||||
|       if (results.length > 0 && results.every((res) => !res.success)) { | ||||
|         const error = { | ||||
|  | @ -368,30 +355,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         return res.status(500).json(error); | ||||
|       } | ||||
| 
 | ||||
|       referencesToCreate = results.map((result) => { | ||||
|         return { | ||||
|           type: result.type, | ||||
|           uid: result.createdEvent.id.toString(), | ||||
|         }; | ||||
|       }); | ||||
|       // Forward results
 | ||||
|       results = createResults.results; | ||||
|       referencesToCreate = createResults.referencesToCreate; | ||||
|     } | ||||
| 
 | ||||
|     const hashUID = | ||||
|       results.length > 0 ? results[0].uid : translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL)); | ||||
|     // TODO Should just be set to the true case as soon as we have a "bare email" integration class.
 | ||||
|     // UID generation should happen in the integration itself, not here.
 | ||||
|     if (results.length === 0) { | ||||
|       // Legacy as well, as soon as we have a separate email integration class. Just used
 | ||||
|       // to send an email even if there is no integration at all.
 | ||||
|       try { | ||||
|         const mail = new EventAttendeeMail(evt, hashUID); | ||||
|         await mail.sendEmail(); | ||||
|       } catch (e) { | ||||
|         log.error("Sending legacy event mail failed", e); | ||||
|         log.error(`Booking ${user} failed`); | ||||
|         res.status(500).json({ message: "Booking failed" }); | ||||
|         return; | ||||
|       } | ||||
|     const legacyMailError = await handleLegacyConfirmationMail(results, selectedEventType, evt, hashUID); | ||||
|     if (legacyMailError) { | ||||
|       log.error("Sending legacy event mail failed", legacyMailError.error); | ||||
|       log.error(`Booking ${user} failed`); | ||||
|       res.status(500).json({ message: legacyMailError.message }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|  | @ -410,6 +388,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|           attendees: { | ||||
|             create: evt.attendees, | ||||
|           }, | ||||
|           confirmed: !selectedEventType.requiresConfirmation, | ||||
|         }, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|  | @ -418,6 +397,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (selectedEventType.requiresConfirmation) { | ||||
|       await new EventOrganizerRequestMail(evt, hashUID).sendEmail(); | ||||
|     } | ||||
| 
 | ||||
|     log.debug(`Booking ${user} completed`); | ||||
|     return res.status(204).json({ message: "Booking completed" }); | ||||
|   } catch (reason) { | ||||
|  |  | |||
							
								
								
									
										106
									
								
								pages/api/book/confirm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								pages/api/book/confirm.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||
| import { getSession } from "next-auth/client"; | ||||
| import prisma from "../../../lib/prisma"; | ||||
| import { handleLegacyConfirmationMail } from "./[user]"; | ||||
| import { CalendarEvent } from "@lib/calendarClient"; | ||||
| import EventRejectionMail from "@lib/emails/EventRejectionMail"; | ||||
| import EventManager from "@lib/events/EventManager"; | ||||
| 
 | ||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> { | ||||
|   const session = await getSession({ req: req }); | ||||
|   if (!session) { | ||||
|     return res.status(401).json({ message: "Not authenticated" }); | ||||
|   } | ||||
| 
 | ||||
|   const bookingId = req.body.id; | ||||
|   if (!bookingId) { | ||||
|     return res.status(400).json({ message: "bookingId missing" }); | ||||
|   } | ||||
| 
 | ||||
|   const currentUser = await prisma.user.findFirst({ | ||||
|     where: { | ||||
|       id: session.user.id, | ||||
|     }, | ||||
|     select: { | ||||
|       id: true, | ||||
|       credentials: true, | ||||
|       timeZone: true, | ||||
|       email: true, | ||||
|       name: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (req.method == "PATCH") { | ||||
|     const booking = await prisma.booking.findFirst({ | ||||
|       where: { | ||||
|         id: bookingId, | ||||
|       }, | ||||
|       select: { | ||||
|         title: true, | ||||
|         description: true, | ||||
|         startTime: true, | ||||
|         endTime: true, | ||||
|         confirmed: true, | ||||
|         attendees: true, | ||||
|         userId: true, | ||||
|         id: true, | ||||
|         uid: true, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!booking || booking.userId != currentUser.id) { | ||||
|       return res.status(404).json({ message: "booking not found" }); | ||||
|     } | ||||
|     if (booking.confirmed) { | ||||
|       return res.status(400).json({ message: "booking already confirmed" }); | ||||
|     } | ||||
| 
 | ||||
|     const evt: CalendarEvent = { | ||||
|       type: booking.title, | ||||
|       title: booking.title, | ||||
|       description: booking.description, | ||||
|       startTime: booking.startTime.toISOString(), | ||||
|       endTime: booking.endTime.toISOString(), | ||||
|       organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone }, | ||||
|       attendees: booking.attendees, | ||||
|     }; | ||||
| 
 | ||||
|     if (req.body.confirmed) { | ||||
|       const eventManager = new EventManager(currentUser.credentials); | ||||
|       const scheduleResult = await eventManager.create(evt); | ||||
| 
 | ||||
|       await handleLegacyConfirmationMail( | ||||
|         scheduleResult.results, | ||||
|         { requiresConfirmation: false }, | ||||
|         evt, | ||||
|         booking.uid | ||||
|       ); | ||||
| 
 | ||||
|       await prisma.booking.update({ | ||||
|         where: { | ||||
|           id: bookingId, | ||||
|         }, | ||||
|         data: { | ||||
|           confirmed: true, | ||||
|           references: { | ||||
|             create: scheduleResult.referencesToCreate, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       res.status(204).json({ message: "ok" }); | ||||
|     } else { | ||||
|       await prisma.booking.update({ | ||||
|         where: { | ||||
|           id: bookingId, | ||||
|         }, | ||||
|         data: { | ||||
|           rejected: true, | ||||
|         }, | ||||
|       }); | ||||
|       const attendeeMail = new EventRejectionMail(evt, booking.uid); | ||||
|       await attendeeMail.sendEmail(); | ||||
|       res.status(204).json({ message: "ok" }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										74
									
								
								pages/api/cron/bookingReminder.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								pages/api/cron/bookingReminder.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||
| import prisma from "@lib/prisma"; | ||||
| import dayjs from "dayjs"; | ||||
| import { ReminderType } from "@prisma/client"; | ||||
| import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequestReminderMail"; | ||||
| import { CalendarEvent } from "@lib/calendarClient"; | ||||
| 
 | ||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> { | ||||
|   const apiKey = req.query.apiKey; | ||||
|   if (process.env.CRON_API_KEY != apiKey) { | ||||
|     return res.status(401).json({ message: "Not authenticated" }); | ||||
|   } | ||||
| 
 | ||||
|   if (req.method == "POST") { | ||||
|     const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60]; | ||||
|     let notificationsSent = 0; | ||||
|     for (const interval of reminderIntervalMinutes) { | ||||
|       const bookings = await prisma.booking.findMany({ | ||||
|         where: { | ||||
|           confirmed: false, | ||||
|           rejected: false, | ||||
|           createdAt: { | ||||
|             lte: dayjs().add(-interval, "minutes").toDate(), | ||||
|           }, | ||||
|         }, | ||||
|         select: { | ||||
|           title: true, | ||||
|           description: true, | ||||
|           startTime: true, | ||||
|           endTime: true, | ||||
|           attendees: true, | ||||
|           user: true, | ||||
|           id: true, | ||||
|           uid: true, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       const reminders = await prisma.reminderMail.findMany({ | ||||
|         where: { | ||||
|           reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, | ||||
|           referenceId: { | ||||
|             in: bookings.map((b) => b.id), | ||||
|           }, | ||||
|           elapsedMinutes: { | ||||
|             gte: interval, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) { | ||||
|         const evt: CalendarEvent = { | ||||
|           type: booking.title, | ||||
|           title: booking.title, | ||||
|           description: booking.description, | ||||
|           startTime: booking.startTime.toISOString(), | ||||
|           endTime: booking.endTime.toISOString(), | ||||
|           organizer: { email: booking.user.email, name: booking.user.name, timeZone: booking.user.timeZone }, | ||||
|           attendees: booking.attendees, | ||||
|         }; | ||||
| 
 | ||||
|         await new EventOrganizerRequestReminderMail(evt, booking.uid).sendEmail(); | ||||
|         await prisma.reminderMail.create({ | ||||
|           data: { | ||||
|             referenceId: booking.id, | ||||
|             reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, | ||||
|             elapsedMinutes: interval, | ||||
|           }, | ||||
|         }); | ||||
|         notificationsSent++; | ||||
|       } | ||||
|     } | ||||
|     res.status(200).json({ notificationsSent }); | ||||
|   } | ||||
| } | ||||
|  | @ -66,6 +66,7 @@ type EventTypeInput = { | |||
|   periodStartDate?: Date | string; | ||||
|   periodEndDate?: Date | string; | ||||
|   periodCountCalendarDays?: boolean; | ||||
|   enteredRequiresConfirmation: boolean; | ||||
| }; | ||||
| 
 | ||||
| const PERIOD_TYPES = [ | ||||
|  | @ -172,6 +173,7 @@ export default function EventTypePage({ | |||
|   const descriptionRef = useRef<HTMLTextAreaElement>(); | ||||
|   const lengthRef = useRef<HTMLInputElement>(); | ||||
|   const isHiddenRef = useRef<HTMLInputElement>(); | ||||
|   const requiresConfirmationRef = useRef<HTMLInputElement>(); | ||||
|   const eventNameRef = useRef<HTMLInputElement>(); | ||||
|   const periodDaysRef = useRef<HTMLInputElement>(); | ||||
|   const periodDaysTypeRef = useRef<HTMLSelectElement>(); | ||||
|  | @ -188,6 +190,7 @@ export default function EventTypePage({ | |||
|     const enteredDescription: string = descriptionRef.current.value; | ||||
|     const enteredLength: number = parseInt(lengthRef.current.value); | ||||
|     const enteredIsHidden: boolean = isHiddenRef.current.checked; | ||||
|     const enteredRequiresConfirmation: boolean = requiresConfirmationRef.current.checked; | ||||
|     const enteredEventName: string = eventNameRef.current.value; | ||||
| 
 | ||||
|     const type = periodType.type; | ||||
|  | @ -223,6 +226,7 @@ export default function EventTypePage({ | |||
|       periodStartDate: enteredPeriodStartDate, | ||||
|       periodEndDate: enteredPeriodEndDate, | ||||
|       periodCountCalendarDays: enteredPeriodDaysType, | ||||
|       requiresConfirmation: enteredRequiresConfirmation, | ||||
|     }; | ||||
| 
 | ||||
|     if (enteredAvailability) { | ||||
|  | @ -641,6 +645,29 @@ export default function EventTypePage({ | |||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="my-8"> | ||||
|                     <div className="relative flex items-start"> | ||||
|                       <div className="flex items-center h-5"> | ||||
|                         <input | ||||
|                           ref={requiresConfirmationRef} | ||||
|                           id="requiresConfirmation" | ||||
|                           name="requiresConfirmation" | ||||
|                           type="checkbox" | ||||
|                           className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" | ||||
|                           defaultChecked={eventType.requiresConfirmation} | ||||
|                         /> | ||||
|                       </div> | ||||
|                       <div className="ml-3 text-sm"> | ||||
|                         <label htmlFor="requiresConfirmation" className="font-medium text-gray-700"> | ||||
|                           Booking requires manual confirmation | ||||
|                         </label> | ||||
|                         <p className="text-gray-500"> | ||||
|                           The booking needs to be confirmed, before it is pushed to the integrations and a | ||||
|                           confirmation mail is sent. | ||||
|                         </p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
| 
 | ||||
|                   <fieldset className="my-8"> | ||||
|                     <Text variant="largetitle">When can people book this event?</Text> | ||||
|  | @ -991,6 +1018,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query | |||
|       periodStartDate: true, | ||||
|       periodEndDate: true, | ||||
|       periodCountCalendarDays: true, | ||||
|       requiresConfirmation: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,14 +2,29 @@ import Head from "next/head"; | |||
| import prisma from "../../lib/prisma"; | ||||
| import { getSession, useSession } from "next-auth/client"; | ||||
| import Shell from "../../components/Shell"; | ||||
| import { useRouter } from "next/router"; | ||||
| 
 | ||||
| export default function Bookings({ bookings }) { | ||||
|   const [, loading] = useSession(); | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   if (loading) { | ||||
|     return <p className="text-gray-400">Loading...</p>; | ||||
|   } | ||||
| 
 | ||||
|   async function confirmBookingHandler(booking, confirm: boolean) { | ||||
|     const res = await fetch("/api/book/confirm", { | ||||
|       method: "PATCH", | ||||
|       body: JSON.stringify({ id: booking.id, confirmed: confirm }), | ||||
|       headers: { | ||||
|         "Content-Type": "application/json", | ||||
|       }, | ||||
|     }); | ||||
|     if (res.ok) { | ||||
|       await router.replace(router.asPath); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <Head> | ||||
|  | @ -45,35 +60,72 @@ export default function Bookings({ bookings }) { | |||
|                     </tr> | ||||
|                   </thead> | ||||
|                   <tbody className="bg-white divide-y divide-gray-200"> | ||||
|                     {bookings.map((booking) => ( | ||||
|                       <tr key={booking.id}> | ||||
|                         <td className="px-6 py-4 whitespace-nowrap"> | ||||
|                           <div className="text-sm font-medium text-gray-900">{booking.attendees[0].name}</div> | ||||
|                           <div className="text-sm text-gray-500">{booking.attendees[0].email}</div> | ||||
|                         </td> | ||||
|                         <td className="px-6 py-4 whitespace-nowrap"> | ||||
|                           <div className="text-sm text-gray-900">{booking.title}</div> | ||||
|                           <div className="text-sm text-gray-500">{booking.description}</div> | ||||
|                         </td> | ||||
|                         {/* <td className="px-6 py-4 whitespace-nowrap"> | ||||
|                     {bookings | ||||
|                       .filter((booking) => !booking.confirmed && !booking.rejected) | ||||
|                       .concat(bookings.filter((booking) => booking.confirmed || booking.rejected)) | ||||
|                       .map((booking) => ( | ||||
|                         <tr key={booking.id}> | ||||
|                           <td | ||||
|                             className={ | ||||
|                               "px-6 py-4 whitespace-nowrap" + (booking.rejected ? " line-through" : "") | ||||
|                             }> | ||||
|                             {!booking.confirmed && !booking.rejected && ( | ||||
|                               <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-600 text-gray-100"> | ||||
|                                 Unconfirmed | ||||
|                               </span> | ||||
|                             )} | ||||
|                             <div className="text-sm font-medium text-gray-900"> | ||||
|                               {booking.attendees[0].name} | ||||
|                             </div> | ||||
|                             <div className="text-sm text-gray-500">{booking.attendees[0].email}</div> | ||||
|                           </td> | ||||
|                           <td | ||||
|                             className={ | ||||
|                               "px-6 py-4 whitespace-nowrap" + (booking.rejected ? " line-through" : "") | ||||
|                             }> | ||||
|                             <div className="text-sm text-gray-900">{booking.title}</div> | ||||
|                             <div className="text-sm text-gray-500">{booking.description}</div> | ||||
|                           </td> | ||||
|                           {/* <td className="px-6 py-4 whitespace-nowrap"> | ||||
|                           <div className="text-sm text-gray-500"> | ||||
|                             {dayjs(booking.startTime).format("D MMMM YYYY HH:mm")} | ||||
|                           </div> | ||||
|                         </td> */} | ||||
|                         <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | ||||
|                           <a | ||||
|                             href={window.location.href + "/../reschedule/" + booking.uid} | ||||
|                             className="text-blue-600 hover:text-blue-900"> | ||||
|                             Reschedule | ||||
|                           </a> | ||||
|                           <a | ||||
|                             href={window.location.href + "/../cancel/" + booking.uid} | ||||
|                             className="ml-4 text-blue-600 hover:text-blue-900"> | ||||
|                             Cancel | ||||
|                           </a> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     ))} | ||||
|                           <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | ||||
|                             {!booking.confirmed && !booking.rejected && ( | ||||
|                               <> | ||||
|                                 <a | ||||
|                                   onClick={() => confirmBookingHandler(booking, true)} | ||||
|                                   className="cursor-pointer text-blue-600 hover:text-blue-900"> | ||||
|                                   Confirm | ||||
|                                 </a> | ||||
|                                 <a | ||||
|                                   onClick={() => confirmBookingHandler(booking, false)} | ||||
|                                   className="cursor-pointer ml-4 text-blue-600 hover:text-blue-900"> | ||||
|                                   Reject | ||||
|                                 </a> | ||||
|                               </> | ||||
|                             )} | ||||
|                             {booking.confirmed && !booking.rejected && ( | ||||
|                               <> | ||||
|                                 <a | ||||
|                                   href={window.location.href + "/../reschedule/" + booking.uid} | ||||
|                                   className="text-blue-600 hover:text-blue-900"> | ||||
|                                   Reschedule | ||||
|                                 </a> | ||||
|                                 <a | ||||
|                                   href={window.location.href + "/../cancel/" + booking.uid} | ||||
|                                   className="ml-4 text-blue-600 hover:text-blue-900"> | ||||
|                                   Cancel | ||||
|                                 </a> | ||||
|                               </> | ||||
|                             )} | ||||
|                             {!booking.confirmed && booking.rejected && ( | ||||
|                               <div className="text-sm text-gray-500">Rejected</div> | ||||
|                             )} | ||||
|                           </td> | ||||
|                         </tr> | ||||
|                       ))} | ||||
|                   </tbody> | ||||
|                 </table> | ||||
|               </div> | ||||
|  | @ -110,6 +162,9 @@ export async function getServerSideProps(context) { | |||
|       title: true, | ||||
|       description: true, | ||||
|       attendees: true, | ||||
|       confirmed: true, | ||||
|       rejected: true, | ||||
|       id: true, | ||||
|     }, | ||||
|     orderBy: { | ||||
|       startTime: "desc", | ||||
|  |  | |||
|  | @ -1,103 +1,110 @@ | |||
| import Head from 'next/head'; | ||||
| import Link from 'next/link'; | ||||
| import { useState } from 'react'; | ||||
| import { useRouter } from 'next/router'; | ||||
| import prisma from '../../lib/prisma'; | ||||
| import Modal from '../../components/Modal'; | ||||
| import Shell from '../../components/Shell'; | ||||
| import SettingsShell from '../../components/Settings'; | ||||
| import Avatar from '../../components/Avatar'; | ||||
| import { signIn, useSession, getSession } from 'next-auth/client'; | ||||
| import TimezoneSelect from 'react-timezone-select'; | ||||
| import Head from "next/head"; | ||||
| import prisma from "../../lib/prisma"; | ||||
| import Shell from "../../components/Shell"; | ||||
| import SettingsShell from "../../components/Settings"; | ||||
| import { getSession, useSession } from "next-auth/client"; | ||||
| 
 | ||||
| export default function Embed(props) { | ||||
|     const [ session, loading ] = useSession(); | ||||
|     const router = useRouter(); | ||||
|   const [session, loading] = useSession(); | ||||
| 
 | ||||
|     if (loading) { | ||||
|         return <div className="loader"></div>; | ||||
|     } | ||||
|   if (loading) { | ||||
|     return <div className="loader"></div>; | ||||
|   } | ||||
| 
 | ||||
|     return( | ||||
|         <Shell heading="Embed"> | ||||
|             <Head> | ||||
|                 <title>Embed | Calendso</title> | ||||
|                 <link rel="icon" href="/favicon.ico" /> | ||||
|             </Head> | ||||
|             <SettingsShell> | ||||
|                 <div className="py-6 px-4 sm:p-6 lg:pb-8 lg:col-span-9"> | ||||
|                     <div className="mb-6"> | ||||
|                         <h2 className="text-lg leading-6 font-medium text-gray-900">Iframe Embed</h2> | ||||
|                         <p className="mt-1 text-sm text-gray-500"> | ||||
|                             The easiest way to embed Calendso on your website. | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div className="grid grid-cols-2 space-x-4"> | ||||
|                         <div> | ||||
|                             <label htmlFor="iframe" className="block text-sm font-medium text-gray-700"> | ||||
|                                 Standard iframe | ||||
|                             </label> | ||||
|                             <div className="mt-1"> | ||||
|                                 <textarea | ||||
|                                     id="iframe" | ||||
|                                     className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                     placeholder="Loading..." | ||||
|                                     defaultValue={'<iframe src="https://calendso.com/' + session.user.username + '" frameborder="0" allowfullscreen></iframe>'} | ||||
|                                     readOnly | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div> | ||||
|                             <label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700"> | ||||
|                                 Responsive full screen iframe | ||||
|                             </label> | ||||
|                             <div className="mt-1"> | ||||
|                                 <textarea | ||||
|                                     id="fullscreen" | ||||
|                                     className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                     placeholder="Loading..." | ||||
|                                     defaultValue={'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body><iframe src="https://calendso.com/' + session.user.username + '" frameborder="0" allowfullscreen></iframe></body></html>'} | ||||
|                                     readOnly | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div className="my-6"> | ||||
|                         <h2 className="text-lg leading-6 font-medium text-gray-900">Calendso API</h2> | ||||
|                         <p className="mt-1 text-sm text-gray-500"> | ||||
|                             Leverage our API for full control and customizability. | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <a href="https://api.docs.calendso.com" className="btn btn-primary">Browse our API documentation</a> | ||||
|                 </div> | ||||
|             </SettingsShell> | ||||
|         </Shell> | ||||
|     ); | ||||
|   return ( | ||||
|     <Shell heading="Embed"> | ||||
|       <Head> | ||||
|         <title>Embed | Calendso</title> | ||||
|         <link rel="icon" href="/favicon.ico" /> | ||||
|       </Head> | ||||
|       <SettingsShell> | ||||
|         <div className="py-6 px-4 sm:p-6 lg:pb-8 lg:col-span-9"> | ||||
|           <div className="mb-6"> | ||||
|             <h2 className="text-lg leading-6 font-medium text-gray-900">Iframe Embed</h2> | ||||
|             <p className="mt-1 text-sm text-gray-500">The easiest way to embed Calendso on your website.</p> | ||||
|           </div> | ||||
|           <div className="grid grid-cols-2 space-x-4"> | ||||
|             <div> | ||||
|               <label htmlFor="iframe" className="block text-sm font-medium text-gray-700"> | ||||
|                 Standard iframe | ||||
|               </label> | ||||
|               <div className="mt-1"> | ||||
|                 <textarea | ||||
|                   id="iframe" | ||||
|                   className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                   placeholder="Loading..." | ||||
|                   defaultValue={ | ||||
|                     '<iframe src="' + | ||||
|                     props.BASE_URL + | ||||
|                     "/" + | ||||
|                     session.user.username + | ||||
|                     '" frameborder="0" allowfullscreen></iframe>' | ||||
|                   } | ||||
|                   readOnly | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div> | ||||
|               <label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700"> | ||||
|                 Responsive full screen iframe | ||||
|               </label> | ||||
|               <div className="mt-1"> | ||||
|                 <textarea | ||||
|                   id="fullscreen" | ||||
|                   className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                   placeholder="Loading..." | ||||
|                   defaultValue={ | ||||
|                     '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body><iframe src="' + | ||||
|                     props.BASE_URL + | ||||
|                     "/" + | ||||
|                     session.user.username + | ||||
|                     '" frameborder="0" allowfullscreen></iframe></body></html>' | ||||
|                   } | ||||
|                   readOnly | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="my-6"> | ||||
|             <h2 className="text-lg leading-6 font-medium text-gray-900">Calendso API</h2> | ||||
|             <p className="mt-1 text-sm text-gray-500"> | ||||
|               Leverage our API for full control and customizability. | ||||
|             </p> | ||||
|           </div> | ||||
|           <a href="https://api.docs.calendso.com" className="btn btn-primary"> | ||||
|             Browse our API documentation | ||||
|           </a> | ||||
|         </div> | ||||
|       </SettingsShell> | ||||
|     </Shell> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getServerSideProps(context) { | ||||
|     const session = await getSession(context); | ||||
|     if (!session) { | ||||
|         return { redirect: { permanent: false, destination: '/auth/login' } }; | ||||
|     } | ||||
|   const session = await getSession(context); | ||||
|   if (!session) { | ||||
|     return { redirect: { permanent: false, destination: "/auth/login" } }; | ||||
|   } | ||||
| 
 | ||||
|     const user = await prisma.user.findFirst({ | ||||
|         where: { | ||||
|             email: session.user.email, | ||||
|         }, | ||||
|         select: { | ||||
|             id: true, | ||||
|             username: true, | ||||
|             name: true, | ||||
|             email: true, | ||||
|             bio: true, | ||||
|             avatar: true, | ||||
|             timeZone: true, | ||||
|             weekStart: true, | ||||
|         } | ||||
|     }); | ||||
|   const user = await prisma.user.findFirst({ | ||||
|     where: { | ||||
|       email: session.user.email, | ||||
|     }, | ||||
|     select: { | ||||
|       id: true, | ||||
|       username: true, | ||||
|       name: true, | ||||
|       email: true, | ||||
|       bio: true, | ||||
|       avatar: true, | ||||
|       timeZone: true, | ||||
|       weekStart: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|     return { | ||||
|       props: {user}, // will be passed to the page component as props
 | ||||
|     } | ||||
| } | ||||
|   const BASE_URL = process.env.BASE_URL; | ||||
| 
 | ||||
|   return { | ||||
|     props: { user, BASE_URL }, // will be passed to the page component as props
 | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -62,7 +62,10 @@ export default function Success(props) { | |||
|     isReady && ( | ||||
|       <div> | ||||
|         <Head> | ||||
|           <title>Booking Confirmed | {eventName} | Calendso</title> | ||||
|           <title> | ||||
|             Booking {props.eventType.requiresConfirmation ? "Submitted" : "Confirmed"} | {eventName} | | ||||
|             Calendso | ||||
|           </title> | ||||
|           <link rel="icon" href="/favicon.ico" /> | ||||
|         </Head> | ||||
|         <main className="max-w-3xl mx-auto my-24"> | ||||
|  | @ -79,17 +82,26 @@ export default function Success(props) { | |||
|                   aria-labelledby="modal-headline"> | ||||
|                   <div> | ||||
|                     <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100"> | ||||
|                       <CheckIcon className="h-6 w-6 text-green-600" /> | ||||
|                       {!props.eventType.requiresConfirmation && ( | ||||
|                         <CheckIcon className="h-6 w-6 text-green-600" /> | ||||
|                       )} | ||||
|                       {props.eventType.requiresConfirmation && ( | ||||
|                         <ClockIcon className="h-6 w-6 text-green-600" /> | ||||
|                       )} | ||||
|                     </div> | ||||
|                     <div className="mt-3 text-center sm:mt-5"> | ||||
|                       <h3 | ||||
|                         className="text-lg leading-6 font-medium dark:text-white text-gray-900" | ||||
|                         id="modal-headline"> | ||||
|                         Booking confirmed | ||||
|                         Booking {props.eventType.requiresConfirmation ? "Submitted" : "Confirmed"} | ||||
|                       </h3> | ||||
|                       <div className="mt-2"> | ||||
|                         <p className="text-sm text-gray-500 dark:text-gray-300"> | ||||
|                           You are scheduled in with {props.user.name || props.user.username}. | ||||
|                           {props.eventType.requiresConfirmation | ||||
|                             ? `${ | ||||
|                                 props.user.name || props.user.username | ||||
|                               } still needs to confirm or reject the booking.` | ||||
|                             : `You are scheduled in with ${props.user.name || props.user.username}.`} | ||||
|                         </p> | ||||
|                       </div> | ||||
|                       <div className="mt-4 border-t border-b dark:border-gray-900 py-4"> | ||||
|  | @ -113,93 +125,97 @@ export default function Success(props) { | |||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mt-5 sm:mt-0 pt-2 text-center"> | ||||
|                     <span className="font-medium text-gray-500 dark:text-gray-50">Add to your calendar</span> | ||||
|                     <div className="flex mt-2"> | ||||
|                       <Link | ||||
|                         href={ | ||||
|                           `https://calendar.google.com/calendar/r/eventedit?dates=${date | ||||
|                             .utc() | ||||
|                             .format("YYYYMMDDTHHmmss[Z]")}/${date | ||||
|                             .add(props.eventType.length, "minute") | ||||
|                             .utc() | ||||
|                             .format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${ | ||||
|                             props.eventType.description | ||||
|                           }` + (location ? "&location=" + encodeURIComponent(location) : "")
 | ||||
|                         }> | ||||
|                         <a className="mx-2 btn-wide btn-white"> | ||||
|                           <svg | ||||
|                             className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                             fill="currentColor" | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             viewBox="0 0 24 24"> | ||||
|                             <title>Google</title> | ||||
|                             <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" /> | ||||
|                           </svg> | ||||
|                         </a> | ||||
|                       </Link> | ||||
|                       <Link | ||||
|                         href={ | ||||
|                           encodeURI( | ||||
|                             "https://outlook.live.com/calendar/0/deeplink/compose?body=" + | ||||
|                               props.eventType.description + | ||||
|                               "&enddt=" + | ||||
|                               date.add(props.eventType.length, "minute").format() + | ||||
|                               "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + | ||||
|                               date.format() + | ||||
|                               "&subject=" + | ||||
|                               eventName | ||||
|                           ) + (location ? "&location=" + location : "") | ||||
|                         }> | ||||
|                         <a className="mx-2 btn-wide btn-white"> | ||||
|                           <svg | ||||
|                             className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                             fill="currentColor" | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             viewBox="0 0 24 24"> | ||||
|                             <title>Microsoft Outlook</title> | ||||
|                             <path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" /> | ||||
|                           </svg> | ||||
|                         </a> | ||||
|                       </Link> | ||||
|                       <Link | ||||
|                         href={ | ||||
|                           encodeURI( | ||||
|                             "https://outlook.office.com/calendar/0/deeplink/compose?body=" + | ||||
|                               props.eventType.description + | ||||
|                               "&enddt=" + | ||||
|                               date.add(props.eventType.length, "minute").format() + | ||||
|                               "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + | ||||
|                               date.format() + | ||||
|                               "&subject=" + | ||||
|                               eventName | ||||
|                           ) + (location ? "&location=" + location : "") | ||||
|                         }> | ||||
|                         <a className="mx-2 btn-wide btn-white"> | ||||
|                           <svg | ||||
|                             className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                             fill="currentColor" | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             viewBox="0 0 24 24"> | ||||
|                             <title>Microsoft Office</title> | ||||
|                             <path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" /> | ||||
|                           </svg> | ||||
|                         </a> | ||||
|                       </Link> | ||||
|                       <Link href={"data:text/calendar," + eventLink()}> | ||||
|                         <a className="mx-2 btn-wide btn-white" download={props.eventType.title + ".ics"}> | ||||
|                           <svg | ||||
|                             version="1.1" | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             viewBox="0 0 1000 1000" | ||||
|                             className="inline-block w-4 h-4 mr-1 -mt-1"> | ||||
|                             <title>Other</title> | ||||
|                             <path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" /> | ||||
|                           </svg> | ||||
|                         </a> | ||||
|                       </Link> | ||||
|                   {!props.eventType.requiresConfirmation && ( | ||||
|                     <div className="mt-5 sm:mt-0 pt-2 text-center"> | ||||
|                       <span className="font-medium text-gray-500 dark:text-gray-50"> | ||||
|                         Add to your calendar | ||||
|                       </span> | ||||
|                       <div className="flex mt-2"> | ||||
|                         <Link | ||||
|                           href={ | ||||
|                             `https://calendar.google.com/calendar/r/eventedit?dates=${date | ||||
|                               .utc() | ||||
|                               .format("YYYYMMDDTHHmmss[Z]")}/${date | ||||
|                               .add(props.eventType.length, "minute") | ||||
|                               .utc() | ||||
|                               .format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${ | ||||
|                               props.eventType.description | ||||
|                             }` + (location ? "&location=" + encodeURIComponent(location) : "")
 | ||||
|                           }> | ||||
|                           <a className="mx-2 btn-wide btn-white"> | ||||
|                             <svg | ||||
|                               className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                               fill="currentColor" | ||||
|                               xmlns="http://www.w3.org/2000/svg" | ||||
|                               viewBox="0 0 24 24"> | ||||
|                               <title>Google</title> | ||||
|                               <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" /> | ||||
|                             </svg> | ||||
|                           </a> | ||||
|                         </Link> | ||||
|                         <Link | ||||
|                           href={ | ||||
|                             encodeURI( | ||||
|                               "https://outlook.live.com/calendar/0/deeplink/compose?body=" + | ||||
|                                 props.eventType.description + | ||||
|                                 "&enddt=" + | ||||
|                                 date.add(props.eventType.length, "minute").format() + | ||||
|                                 "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + | ||||
|                                 date.format() + | ||||
|                                 "&subject=" + | ||||
|                                 eventName | ||||
|                             ) + (location ? "&location=" + location : "") | ||||
|                           }> | ||||
|                           <a className="mx-2 btn-wide btn-white"> | ||||
|                             <svg | ||||
|                               className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                               fill="currentColor" | ||||
|                               xmlns="http://www.w3.org/2000/svg" | ||||
|                               viewBox="0 0 24 24"> | ||||
|                               <title>Microsoft Outlook</title> | ||||
|                               <path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" /> | ||||
|                             </svg> | ||||
|                           </a> | ||||
|                         </Link> | ||||
|                         <Link | ||||
|                           href={ | ||||
|                             encodeURI( | ||||
|                               "https://outlook.office.com/calendar/0/deeplink/compose?body=" + | ||||
|                                 props.eventType.description + | ||||
|                                 "&enddt=" + | ||||
|                                 date.add(props.eventType.length, "minute").format() + | ||||
|                                 "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + | ||||
|                                 date.format() + | ||||
|                                 "&subject=" + | ||||
|                                 eventName | ||||
|                             ) + (location ? "&location=" + location : "") | ||||
|                           }> | ||||
|                           <a className="mx-2 btn-wide btn-white"> | ||||
|                             <svg | ||||
|                               className="inline-block w-4 h-4 mr-1 -mt-1" | ||||
|                               fill="currentColor" | ||||
|                               xmlns="http://www.w3.org/2000/svg" | ||||
|                               viewBox="0 0 24 24"> | ||||
|                               <title>Microsoft Office</title> | ||||
|                               <path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" /> | ||||
|                             </svg> | ||||
|                           </a> | ||||
|                         </Link> | ||||
|                         <Link href={"data:text/calendar," + eventLink()}> | ||||
|                           <a className="mx-2 btn-wide btn-white" download={props.eventType.title + ".ics"}> | ||||
|                             <svg | ||||
|                               version="1.1" | ||||
|                               xmlns="http://www.w3.org/2000/svg" | ||||
|                               viewBox="0 0 1000 1000" | ||||
|                               className="inline-block w-4 h-4 mr-1 -mt-1"> | ||||
|                               <title>Other</title> | ||||
|                               <path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" /> | ||||
|                             </svg> | ||||
|                           </a> | ||||
|                         </Link> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   )} | ||||
|                   {!props.user.hideBranding && ( | ||||
|                     <div className="mt-4 pt-4 border-t dark:border-gray-900  text-gray-400 text-center text-xs dark:text-white"> | ||||
|                       <a href="https://checkout.calendso.com">Create your own booking link with Calendso</a> | ||||
|  | @ -237,7 +253,7 @@ export async function getServerSideProps(context) { | |||
|     { | ||||
|       id: parseInt(context.query.type), | ||||
|     }, | ||||
|     ["id", "title", "description", "length", "eventName"] | ||||
|     ["id", "title", "description", "length", "eventName", "requiresConfirmation"] | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|  |  | |||
|  | @ -0,0 +1,6 @@ | |||
| -- AlterTable | ||||
| ALTER TABLE "Booking" ADD COLUMN     "confirmed" BOOLEAN NOT NULL DEFAULT true, | ||||
| ADD COLUMN     "rejected" BOOLEAN NOT NULL DEFAULT false; | ||||
| 
 | ||||
| -- AlterTable | ||||
| ALTER TABLE "EventType" ADD COLUMN     "requiresConfirmation" BOOLEAN NOT NULL DEFAULT false; | ||||
|  | @ -0,0 +1,13 @@ | |||
| -- CreateEnum | ||||
| CREATE TYPE "ReminderType" AS ENUM ('PENDING_BOOKING_CONFIRMATION'); | ||||
| 
 | ||||
| -- CreateTable | ||||
| CREATE TABLE "ReminderMail" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "referenceId" INTEGER NOT NULL, | ||||
|     "reminderType" "ReminderType" NOT NULL, | ||||
|     "elapsedMinutes" INTEGER NOT NULL, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
| 
 | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
|  | @ -30,6 +30,7 @@ model EventType { | |||
|   periodEndDate DateTime? | ||||
|   periodDays Int? | ||||
|   periodCountCalendarDays Boolean? | ||||
|   requiresConfirmation Boolean @default(false) | ||||
| } | ||||
| 
 | ||||
| model Credential { | ||||
|  | @ -134,6 +135,8 @@ model Booking { | |||
| 
 | ||||
|   createdAt     DateTime    @default(now()) | ||||
|   updatedAt     DateTime? | ||||
|   confirmed     Boolean @default(true) | ||||
|   rejected      Boolean @default(false) | ||||
| } | ||||
| 
 | ||||
| model Availability { | ||||
|  | @ -173,3 +176,15 @@ model ResetPasswordRequest { | |||
|   email      String | ||||
|   expires    DateTime | ||||
| } | ||||
| 
 | ||||
| enum ReminderType { | ||||
|   PENDING_BOOKING_CONFIRMATION | ||||
| } | ||||
| 
 | ||||
| model ReminderMail { | ||||
|   id             Int      @id @default(autoincrement()) | ||||
|   referenceId    Int | ||||
|   reminderType   ReminderType | ||||
|   elapsedMinutes Int | ||||
|   createdAt      DateTime @default(now()) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas