diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx
new file mode 100644
index 0000000000..cfcc6a79cc
--- /dev/null
+++ b/components/booking/DatePicker.tsx
@@ -0,0 +1,135 @@
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
+import { useEffect, useState } from "react";
+import dayjs, { Dayjs } from "dayjs";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
+import getSlots from "@lib/slots";
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+const DatePicker = ({
+ weekStart,
+ onDatePicked,
+ workingHours,
+ organizerTimeZone,
+ inviteeTimeZone,
+ eventLength,
+}) => {
+ const [calendar, setCalendar] = useState([]);
+ const [selectedMonth, setSelectedMonth]: number = useState();
+ const [selectedDate, setSelectedDate]: Dayjs = useState();
+
+ useEffect(() => {
+ setSelectedMonth(dayjs().tz(inviteeTimeZone).month());
+ }, []);
+
+ useEffect(() => {
+ if (selectedDate) onDatePicked(selectedDate);
+ }, [selectedDate]);
+
+ // Handle month changes
+ const incrementMonth = () => {
+ setSelectedMonth(selectedMonth + 1);
+ };
+
+ const decrementMonth = () => {
+ setSelectedMonth(selectedMonth - 1);
+ };
+
+ useEffect(() => {
+ if (!selectedMonth) {
+ // wish next had a way of dealing with this magically;
+ return;
+ }
+
+ const inviteeDate = dayjs().tz(inviteeTimeZone).month(selectedMonth);
+
+ const isDisabled = (day: number) => {
+ const date: Dayjs = inviteeDate.date(day);
+ return (
+ date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
+ !getSlots({
+ inviteeDate: date,
+ frequency: eventLength,
+ workingHours,
+ organizerTimeZone,
+ }).length
+ );
+ };
+
+ // Set up calendar
+ const daysInMonth = inviteeDate.daysInMonth();
+ const days = [];
+ for (let i = 1; i <= daysInMonth; i++) {
+ days.push(i);
+ }
+
+ // Create placeholder elements for empty days in first week
+ let weekdayOfFirst = inviteeDate.date(1).day();
+ if (weekStart === "Monday") {
+ weekdayOfFirst -= 1;
+ if (weekdayOfFirst < 0) weekdayOfFirst = 6;
+ }
+ const emptyDays = Array(weekdayOfFirst)
+ .fill(null)
+ .map((day, i) => (
+
+ ));
+
+ // Combine placeholder days with actual days
+ setCalendar([
+ ...emptyDays,
+ ...days.map((day) => (
+
+ )),
+ ]);
+ }, [selectedMonth, inviteeTimeZone, selectedDate]);
+
+ return selectedMonth ? (
+
+ ) : null;
+};
+
+export default DatePicker;
diff --git a/components/booking/Slots.tsx b/components/booking/Slots.tsx
new file mode 100644
index 0000000000..1b4a8bfd21
--- /dev/null
+++ b/components/booking/Slots.tsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from "react";
+import { useRouter } from "next/router";
+import getSlots from "../../lib/slots";
+import dayjs, { Dayjs } from "dayjs";
+import isBetween from "dayjs/plugin/isBetween";
+import utc from "dayjs/plugin/utc";
+
+dayjs.extend(isBetween);
+dayjs.extend(utc);
+
+type Props = {
+ eventLength: number;
+ minimumBookingNotice?: number;
+ date: Dayjs;
+};
+
+const Slots = ({ eventLength, minimumBookingNotice, date, workingHours, organizerUtcOffset }: Props) => {
+ minimumBookingNotice = minimumBookingNotice || 0;
+
+ const router = useRouter();
+ const { user } = router.query;
+ const [slots, setSlots] = useState([]);
+ const [isFullyBooked, setIsFullyBooked] = useState(false);
+ const [hasErrors, setHasErrors] = useState(false);
+
+ useEffect(() => {
+ setSlots([]);
+ setIsFullyBooked(false);
+ setHasErrors(false);
+ fetch(
+ `/api/availability/${user}?dateFrom=${date.startOf("day").utc().startOf("day").format()}&dateTo=${date
+ .endOf("day")
+ .utc()
+ .endOf("day")
+ .format()}`
+ )
+ .then((res) => res.json())
+ .then(handleAvailableSlots)
+ .catch((e) => {
+ console.error(e);
+ setHasErrors(true);
+ });
+ }, [date]);
+
+ const handleAvailableSlots = (busyTimes: []) => {
+ const times = getSlots({
+ frequency: eventLength,
+ inviteeDate: date,
+ workingHours,
+ minimumBookingNotice,
+ organizerUtcOffset,
+ });
+
+ const timesLengthBeforeConflicts: number = times.length;
+
+ // Check for conflicts
+ for (let i = times.length - 1; i >= 0; i -= 1) {
+ busyTimes.every((busyTime): boolean => {
+ const startTime = dayjs(busyTime.start).utc();
+ const endTime = dayjs(busyTime.end).utc();
+ // Check if start times are the same
+ if (times[i].utc().isSame(startTime)) {
+ times.splice(i, 1);
+ }
+ // Check if time is between start and end times
+ else if (times[i].utc().isBetween(startTime, endTime)) {
+ times.splice(i, 1);
+ }
+ // Check if slot end time is between start and end time
+ else if (times[i].utc().add(eventLength, "minutes").isBetween(startTime, endTime)) {
+ times.splice(i, 1);
+ }
+ // Check if startTime is between slot
+ else if (startTime.isBetween(times[i].utc(), times[i].utc().add(eventLength, "minutes"))) {
+ times.splice(i, 1);
+ } else {
+ return true;
+ }
+ return false;
+ });
+ }
+
+ if (times.length === 0 && timesLengthBeforeConflicts !== 0) {
+ setIsFullyBooked(true);
+ }
+ // Display available times
+ setSlots(times);
+ };
+
+ return {
+ slots,
+ isFullyBooked,
+ hasErrors,
+ };
+};
+
+export default Slots;
diff --git a/components/booking/TimeOptions.tsx b/components/booking/TimeOptions.tsx
index 38aafd8bf5..a785789f1f 100644
--- a/components/booking/TimeOptions.tsx
+++ b/components/booking/TimeOptions.tsx
@@ -1,73 +1,72 @@
-import {Switch} from "@headlessui/react";
+import { Switch } from "@headlessui/react";
import TimezoneSelect from "react-timezone-select";
-import {useEffect, useState} from "react";
-import {timeZone, is24h} from '../../lib/clock';
+import { useEffect, useState } from "react";
+import { is24h, timeZone } from "../../lib/clock";
function classNames(...classes) {
- return classes.filter(Boolean).join(' ')
+ return classes.filter(Boolean).join(" ");
}
const TimeOptions = (props) => {
-
- const [selectedTimeZone, setSelectedTimeZone] = useState('');
+ const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [is24hClock, setIs24hClock] = useState(false);
- useEffect( () => {
+ useEffect(() => {
setIs24hClock(is24h());
setSelectedTimeZone(timeZone());
}, []);
- useEffect( () => {
- props.onSelectTimeZone(timeZone(selectedTimeZone));
+ useEffect(() => {
+ if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
+ props.onSelectTimeZone(timeZone(selectedTimeZone));
+ }
}, [selectedTimeZone]);
- useEffect( () => {
+ useEffect(() => {
props.onToggle24hClock(is24h(is24hClock));
}, [is24hClock]);
- return selectedTimeZone !== "" && (
-
-
-
Time Options
-
-
-
- am/pm
-
-
- Use setting
-
+
+
Time Options
+
+
+
+ am/pm
+
+
-
-
- 24h
-
-
+ is24hClock ? "bg-blue-600" : "bg-gray-200",
+ "relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+ )}>
+ Use setting
+
+
+
+ 24h
+
+
+
+ setSelectedTimeZone(tz.value)}
+ className="mb-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
+ />
-
setSelectedTimeZone(tz.value)}
- className="mb-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
- />
-
+ )
);
-}
+};
-export default TimeOptions;
\ No newline at end of file
+export default TimeOptions;
diff --git a/components/ui/PoweredByCalendso.tsx b/components/ui/PoweredByCalendso.tsx
new file mode 100644
index 0000000000..a438189ded
--- /dev/null
+++ b/components/ui/PoweredByCalendso.tsx
@@ -0,0 +1,19 @@
+import Link from "next/link";
+
+const PoweredByCalendso = () => (
+
+);
+
+export default PoweredByCalendso;
diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx
new file mode 100644
index 0000000000..edec1319bf
--- /dev/null
+++ b/components/ui/Scheduler.tsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useState } from "react";
+import TimezoneSelect from "react-timezone-select";
+import { TrashIcon } from "@heroicons/react/outline";
+import { WeekdaySelect } from "./WeekdaySelect";
+import SetTimesModal from "./modal/SetTimesModal";
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
+import { Availability } from "@prisma/client";
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+type Props = {
+ timeZone: string;
+ availability: Availability[];
+ setTimeZone: unknown;
+};
+
+export const Scheduler = ({
+ availability,
+ setAvailability,
+ timeZone: selectedTimeZone,
+ setTimeZone,
+}: Props) => {
+ const [editSchedule, setEditSchedule] = useState(-1);
+ const [dateOverrides, setDateOverrides] = useState([]);
+ const [openingHours, setOpeningHours] = useState([]);
+
+ useEffect(() => {
+ setOpeningHours(
+ availability
+ .filter((item: Availability) => item.days.length !== 0)
+ .map((item) => {
+ item.startDate = dayjs().utc().startOf("day").add(item.startTime, "minutes");
+ item.endDate = dayjs().utc().startOf("day").add(item.endTime, "minutes");
+ return item;
+ })
+ );
+ setDateOverrides(availability.filter((item: Availability) => item.date));
+ }, []);
+
+ // updates availability to how it should be formatted outside this component.
+ useEffect(() => {
+ setAvailability({
+ dateOverrides: dateOverrides,
+ openingHours: openingHours,
+ });
+ }, [dateOverrides, openingHours]);
+
+ const addNewSchedule = () => setEditSchedule(openingHours.length);
+
+ const applyEditSchedule = (changed) => {
+ // new entry
+ if (!changed.days) {
+ changed.days = [1, 2, 3, 4, 5]; // Mon - Fri
+ setOpeningHours(openingHours.concat(changed));
+ } else {
+ // update
+ const replaceWith = { ...openingHours[editSchedule], ...changed };
+ openingHours.splice(editSchedule, 1, replaceWith);
+ setOpeningHours([].concat(openingHours));
+ }
+ };
+
+ const removeScheduleAt = (toRemove: number) => {
+ openingHours.splice(toRemove, 1);
+ setOpeningHours([].concat(openingHours));
+ };
+
+ const OpeningHours = ({ idx, item }) => (
+
+
+ (item.days = selected)} />
+ setEditSchedule(idx)}>
+ {dayjs()
+ .startOf("day")
+ .add(item.startTime, "minutes")
+ .format(item.startTime % 60 === 0 ? "ha" : "h:mma")}
+ until
+ {dayjs()
+ .startOf("day")
+ .add(item.endTime, "minutes")
+ .format(item.endTime % 60 === 0 ? "ha" : "h:mma")}
+
+
+ removeScheduleAt(idx)}
+ className="btn-sm bg-transparent px-2 py-1 ml-1">
+
+
+
+ );
+
+ return (
+
+
+
+
+
+ Timezone
+
+
+ setTimeZone(tz.value)}
+ className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
+ />
+
+
+
+ {openingHours.map((item, idx) => (
+
+ ))}
+
+
+
+ Add another
+
+
+
+ {/*
Add date overrides
+
+ Add dates when your availability changes from your weekly hours
+
+
Add a date override */}
+
+
+ {editSchedule >= 0 && (
+
applyEditSchedule({ ...(openingHours[editSchedule] || {}), ...times })}
+ onExit={() => setEditSchedule(-1)}
+ />
+ )}
+ {/*{showDateOverrideModal &&
+
+ }*/}
+
+ );
+};
diff --git a/components/ui/WeekdaySelect.tsx b/components/ui/WeekdaySelect.tsx
new file mode 100644
index 0000000000..a9f371d827
--- /dev/null
+++ b/components/ui/WeekdaySelect.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect, useState } from "react";
+
+export const WeekdaySelect = (props) => {
+ const [activeDays, setActiveDays] = useState(
+ [...Array(7).keys()].map((v, i) => (props.defaultValue || []).includes(i))
+ );
+
+ const days = ["S", "M", "T", "W", "T", "F", "S"];
+
+ useEffect(() => {
+ props.onSelect(activeDays.map((v, idx) => (v ? idx : -1)).filter((v) => v !== -1));
+ }, [activeDays]);
+
+ const toggleDay = (e, idx: number) => {
+ e.preventDefault();
+ activeDays[idx] = !activeDays[idx];
+ setActiveDays([].concat(activeDays));
+ };
+
+ return (
+
+
+ {days.map((day, idx) =>
+ activeDays[idx] ? (
+ toggleDay(e, idx)}
+ style={{ marginLeft: "-2px" }}
+ className={`
+ active focus:outline-none border-2 border-blue-500 px-2 py-1 rounded
+ ${activeDays[idx + 1] ? "rounded-r-none" : ""}
+ ${activeDays[idx - 1] ? "rounded-l-none" : ""}
+ ${idx === 0 ? "rounded-l" : ""}
+ ${idx === days.length - 1 ? "rounded-r" : ""}
+ `}>
+ {day}
+
+ ) : (
+ toggleDay(e, idx)}
+ style={{ marginTop: "1px", marginBottom: "1px" }}
+ className={`border focus:outline-none px-2 py-1 rounded-none ${
+ idx === 0 ? "rounded-l" : "border-l-0"
+ } ${idx === days.length - 1 ? "rounded-r" : ""}`}>
+ {day}
+
+ )
+ )}
+
+
+ );
+};
diff --git a/components/ui/modal/SetTimesModal.tsx b/components/ui/modal/SetTimesModal.tsx
new file mode 100644
index 0000000000..2334802e11
--- /dev/null
+++ b/components/ui/modal/SetTimesModal.tsx
@@ -0,0 +1,146 @@
+import { ClockIcon } from "@heroicons/react/outline";
+import { useRef } from "react";
+
+export default function SetTimesModal(props) {
+ const [startHours, startMinutes] = [Math.floor(props.startTime / 60), props.startTime % 60];
+ const [endHours, endMinutes] = [Math.floor(props.endTime / 60), props.endTime % 60];
+
+ const startHoursRef = useRef
();
+ const startMinsRef = useRef();
+ const endHoursRef = useRef();
+ const endMinsRef = useRef();
+
+ function updateStartEndTimesHandler(event) {
+ event.preventDefault();
+
+ const enteredStartHours = parseInt(startHoursRef.current.value);
+ const enteredStartMins = parseInt(startMinsRef.current.value);
+ const enteredEndHours = parseInt(endHoursRef.current.value);
+ const enteredEndMins = parseInt(endMinsRef.current.value);
+
+ props.onChange({
+ startTime: enteredStartHours * 60 + enteredStartMins,
+ endTime: enteredEndHours * 60 + enteredEndMins,
+ });
+
+ props.onExit(0);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Change when you are available for bookings
+
+
+
Set your work schedule
+
+
+
+
+
Start time
+
+
+ Hours
+
+
+
+
:
+
+
+ Minutes
+
+
+
+
+
+
End time
+
+
+ Hours
+
+
+
+
:
+
+
+ Minutes
+
+
+
+
+
+
+ Save
+
+
+ Cancel
+
+
+
+
+
+ );
+}
diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts
index 80b17de270..364e96baa8 100644
--- a/lib/calendarClient.ts
+++ b/lib/calendarClient.ts
@@ -102,15 +102,13 @@ const o365Auth = (credential) => {
};
};
-// eslint-disable-next-line
interface Person {
name?: string;
email: string;
timeZone: string;
}
-// eslint-disable-next-line
-interface CalendarEvent {
+export interface CalendarEvent {
type: string;
title: string;
startTime: string;
@@ -122,28 +120,25 @@ interface CalendarEvent {
conferenceData?: ConferenceData;
}
-// eslint-disable-next-line
-interface ConferenceData {
- createRequest: any;
+export interface ConferenceData {
+ createRequest: unknown;
}
-// eslint-disable-next-line
-interface IntegrationCalendar {
+export interface IntegrationCalendar {
integration: string;
primary: boolean;
externalId: string;
name: string;
}
-// eslint-disable-next-line
-interface CalendarApiAdapter {
- createEvent(event: CalendarEvent): Promise;
+export interface CalendarApiAdapter {
+ createEvent(event: CalendarEvent): Promise;
updateEvent(uid: string, event: CalendarEvent);
deleteEvent(uid: string);
- getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise;
+ getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise;
listCalendars(): Promise;
}
@@ -375,6 +370,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
auth: myGoogleAuth,
calendarId: "primary",
resource: payload,
+ conferenceDataVersion: 1,
},
function (err, event) {
if (err) {
@@ -508,15 +504,29 @@ const listCalendars = (withCredentials) =>
results.reduce((acc, calendars) => acc.concat(calendars), [])
);
-const createEvent = async (credential, calEvent: CalendarEvent): Promise => {
+const createEvent = async (credential, calEvent: CalendarEvent): Promise => {
const parser: CalEventParser = new CalEventParser(calEvent);
const uid: string = parser.getUid();
const richEvent: CalendarEvent = parser.asRichEvent();
const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null;
- const organizerMail = new EventOrganizerMail(calEvent, uid);
- const attendeeMail = new EventAttendeeMail(calEvent, uid);
+ const maybeHangoutLink = creationResult?.hangoutLink;
+ const maybeEntryPoints = creationResult?.entryPoints;
+ const maybeConferenceData = creationResult?.conferenceData;
+
+ const organizerMail = new EventOrganizerMail(calEvent, uid, {
+ hangoutLink: maybeHangoutLink,
+ conferenceData: maybeConferenceData,
+ entryPoints: maybeEntryPoints,
+ });
+
+ const attendeeMail = new EventAttendeeMail(calEvent, uid, {
+ hangoutLink: maybeHangoutLink,
+ conferenceData: maybeConferenceData,
+ entryPoints: maybeEntryPoints,
+ });
+
try {
await organizerMail.sendEmail();
} catch (e) {
@@ -537,7 +547,7 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise =>
};
};
-const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEvent): Promise => {
+const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEvent): Promise => {
const parser: CalEventParser = new CalEventParser(calEvent);
const newUid: string = parser.getUid();
const richEvent: CalendarEvent = parser.asRichEvent();
@@ -568,7 +578,7 @@ const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEv
};
};
-const deleteEvent = (credential, uid: string): Promise => {
+const deleteEvent = (credential, uid: string): Promise => {
if (credential) {
return calendars([credential])[0].deleteEvent(uid);
}
diff --git a/lib/emails/EventAttendeeMail.ts b/lib/emails/EventAttendeeMail.ts
index cc4cf4603e..3fd73f17ae 100644
--- a/lib/emails/EventAttendeeMail.ts
+++ b/lib/emails/EventAttendeeMail.ts
@@ -4,6 +4,7 @@ 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);
@@ -22,13 +23,13 @@ export default class EventAttendeeMail extends EventMail {
Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format(
"h:mma"
- )}
+ )}
(${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format(
"dddd, LL"
)} is scheduled.
` +
this.getAdditionalBody() +
- (this.calEvent.location ? `Location: ${this.calEvent.location} ` : "") +
+ " " +
`Additional notes:
${this.calEvent.description}
` +
@@ -39,6 +40,38 @@ export default class EventAttendeeMail extends EventMail {
);
}
+ /**
+ * Adds the video call information to the mail body.
+ *
+ * @protected
+ */
+ protected getLocation(): string {
+ if (this.additionInformation?.hangoutLink) {
+ return `Location: ${this.additionInformation?.hangoutLink} `;
+ }
+
+ if (this.additionInformation?.entryPoints && this.additionInformation?.entryPoints.length > 0) {
+ const locations = this.additionInformation?.entryPoints
+ .map((entryPoint) => {
+ return `
+ Join by ${entryPoint.entryPointType}:
+ ${entryPoint.label}
+ `;
+ })
+ .join(" ");
+
+ return `Locations: ${locations}`;
+ }
+
+ return this.calEvent.location ? `Location: ${this.calEvent.location} ` : "";
+ }
+
+ protected getAdditionalBody(): string {
+ return `
+ ${this.getLocation()}
+ `;
+ }
+
/**
* Returns the payload object for the nodemailer.
*
diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts
index 9ff1cabb95..90cee0b5e9 100644
--- a/lib/emails/EventMail.ts
+++ b/lib/emails/EventMail.ts
@@ -1,13 +1,31 @@
-import { CalendarEvent } from "../calendarClient";
-import { serverConfig } from "../serverConfig";
-import nodemailer from "nodemailer";
import CalEventParser from "../CalEventParser";
import { stripHtml } from "./helpers";
+import { CalendarEvent, ConferenceData } from "../calendarClient";
+import { serverConfig } from "../serverConfig";
+import nodemailer from "nodemailer";
+
+interface EntryPoint {
+ entryPointType?: string;
+ uri?: string;
+ label?: string;
+ pin?: string;
+ accessCode?: string;
+ meetingCode?: string;
+ passcode?: string;
+ password?: string;
+}
+
+interface AdditionInformation {
+ conferenceData?: ConferenceData;
+ entryPoints?: EntryPoint[];
+ hangoutLink?: string;
+}
export default abstract class EventMail {
calEvent: CalendarEvent;
parser: CalEventParser;
uid: string;
+ additionInformation?: AdditionInformation;
/**
* An EventMail always consists of a CalendarEvent
@@ -17,10 +35,11 @@ export default abstract class EventMail {
* @param calEvent
* @param uid
*/
- constructor(calEvent: CalendarEvent, uid: string) {
+ constructor(calEvent: CalendarEvent, uid: string, additionInformation: AdditionInformation = null) {
this.calEvent = calEvent;
this.uid = uid;
this.parser = new CalEventParser(calEvent);
+ this.additionInformation = additionInformation;
}
/**
@@ -88,6 +107,8 @@ export default abstract class EventMail {
return "";
}
+ protected abstract getLocation(): string;
+
/**
* Prints out the desired information when an error
* occured while sending the mail.
diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts
index 20409ce2eb..48b5f078f9 100644
--- a/lib/emails/EventOrganizerMail.ts
+++ b/lib/emails/EventOrganizerMail.ts
@@ -34,7 +34,7 @@ export default class EventOrganizerMail extends EventMail {
stripHtml(this.getAdditionalFooter()),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
- attendees: this.calEvent.attendees.map((attendee: any) => ({
+ attendees: this.calEvent.attendees.map((attendee: unknown) => ({
name: attendee.name,
email: attendee.email,
})),
@@ -66,26 +66,51 @@ export default class EventOrganizerMail extends EventMail {
${this.calEvent.attendees[0].email}
` +
this.getAdditionalBody() +
- (this.calEvent.location
- ? `
- Location:
- ${this.calEvent.location}
-
- `
- : "") +
+ " " +
`Invitee Time Zone:
- ${this.calEvent.attendees[0].timeZone}
-
- Additional notes:
- ${this.calEvent.description}
- ` +
+ ${this.calEvent.attendees[0].timeZone}
+
+ Additional notes:
+ ${this.calEvent.description}
+ ` +
this.getAdditionalFooter() +
- `
+ `
`
);
}
+ /**
+ * Adds the video call information to the mail body.
+ *
+ * @protected
+ */
+ protected getLocation(): string {
+ if (this.additionInformation?.hangoutLink) {
+ return `
`;
+ }
+
+ if (this.additionInformation?.entryPoints && this.additionInformation?.entryPoints.length > 0) {
+ const locations = this.additionInformation?.entryPoints
+ .map((entryPoint) => {
+ return `
+ Join by ${entryPoint.entryPointType}:
` : "";
+ }
+
+ protected getAdditionalBody(): string {
+ return `
+ ${this.getLocation()}
+ `;
+ }
/**
* Returns the payload object for the nodemailer.
*
diff --git a/lib/jsonUtils.ts b/lib/jsonUtils.ts
new file mode 100644
index 0000000000..3f617cb0ce
--- /dev/null
+++ b/lib/jsonUtils.ts
@@ -0,0 +1,11 @@
+export const validJson = (jsonString: string) => {
+ try {
+ const o = JSON.parse(jsonString);
+ if (o && typeof o === "object") {
+ return o;
+ }
+ } catch (e) {
+ console.log("Invalid JSON:", e);
+ }
+ return false;
+};
diff --git a/lib/prisma.ts b/lib/prisma.ts
index 4d3f847070..5d75ada2f4 100644
--- a/lib/prisma.ts
+++ b/lib/prisma.ts
@@ -1,9 +1,9 @@
-import { PrismaClient } from '@prisma/client';
+import { PrismaClient } from "@prisma/client";
let prisma: PrismaClient;
-const globalAny:any = global;
+const globalAny: any = global;
-if (process.env.NODE_ENV === 'production') {
+if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient();
} else {
if (!globalAny.prisma) {
@@ -12,4 +12,27 @@ if (process.env.NODE_ENV === 'production') {
prisma = globalAny.prisma;
}
-export default prisma;
\ No newline at end of file
+const pluck = (select: Record