diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index 1bea6caf..cfcc6a79 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -4,6 +4,7 @@ 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); diff --git a/components/booking/Slots.tsx b/components/booking/Slots.tsx index 8f92aaeb..1b4a8bfd 100644 --- a/components/booking/Slots.tsx +++ b/components/booking/Slots.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect } from "react"; +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); diff --git a/components/booking/TimeOptions.tsx b/components/booking/TimeOptions.tsx index 580e174e..a785789f 100644 --- a/components/booking/TimeOptions.tsx +++ b/components/booking/TimeOptions.tsx @@ -1,7 +1,7 @@ import { Switch } from "@headlessui/react"; import TimezoneSelect from "react-timezone-select"; import { useEffect, useState } from "react"; -import { timeZone, is24h } from "../../lib/clock"; +import { is24h, timeZone } from "../../lib/clock"; function classNames(...classes) { return classes.filter(Boolean).join(" "); diff --git a/components/ui/PoweredByCalendso.tsx b/components/ui/PoweredByCalendso.tsx index 2e890fa8..a438189d 100644 --- a/components/ui/PoweredByCalendso.tsx +++ b/components/ui/PoweredByCalendso.tsx @@ -3,10 +3,7 @@ import Link from "next/link"; const PoweredByCalendso = () => (
- + powered by{" "} (
); -export default PoweredByCalendso; \ No newline at end of file +export default PoweredByCalendso; diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx index 045c726d..edec1319 100644 --- a/components/ui/Scheduler.tsx +++ b/components/ui/Scheduler.tsx @@ -7,6 +7,7 @@ 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); diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts new file mode 100644 index 00000000..a38494f2 --- /dev/null +++ b/lib/CalEventParser.ts @@ -0,0 +1,93 @@ +import { CalendarEvent } from "./calendarClient"; +import { v5 as uuidv5 } from "uuid"; +import short from "short-uuid"; +import { stripHtml } from "./emails/helpers"; + +const translator = short(); + +export default class CalEventParser { + calEvent: CalendarEvent; + + constructor(calEvent: CalendarEvent) { + this.calEvent = calEvent; + } + + /** + * Returns a link to reschedule the given booking. + */ + public getRescheduleLink(): string { + return process.env.BASE_URL + "/reschedule/" + this.getUid(); + } + + /** + * Returns a link to cancel the given booking. + */ + public getCancelLink(): string { + return process.env.BASE_URL + "/cancel/" + this.getUid(); + } + + /** + * Returns a unique identifier for the given calendar event. + */ + public getUid(): string { + return translator.fromUUID(uuidv5(JSON.stringify(this.calEvent), uuidv5.URL)); + } + + /** + * Returns a footer section with links to change the event (as HTML). + */ + public getChangeEventFooterHtml(): string { + return `
+Need to change this event?
+Cancel:
${this.getCancelLink()}
+Reschedule: ${this.getRescheduleLink()} + `; + } + + /** + * Returns a footer section with links to change the event (as plain text). + */ + public getChangeEventFooter(): string { + return stripHtml(this.getChangeEventFooterHtml()); + } + + /** + * Returns an extended description with all important information (as HTML). + * + * @protected + */ + public getRichDescriptionHtml(): string { + // This odd indentation is necessary because otherwise the leading tabs will be applied into the event description. + return ( + ` +Event Type:
${this.calEvent.type}
+Invitee Email:
${this.calEvent.attendees[0].email}
+` + + (this.calEvent.location + ? `Location:
${this.calEvent.location}
+` + : "") + + `Invitee Time Zone:
${this.calEvent.attendees[0].timeZone}
+Additional notes:
${this.calEvent.description}
` + + this.getChangeEventFooterHtml() + ); + } + + /** + * Returns an extended description with all important information (as plain text). + * + * @protected + */ + public getRichDescription(): string { + return stripHtml(this.getRichDescriptionHtml()); + } + + /** + * Returns a calendar event with rich description. + */ + public asRichEvent(): CalendarEvent { + const eventCopy: CalendarEvent = { ...this.calEvent }; + eventCopy.description = this.getRichDescriptionHtml(); + return eventCopy; + } +} diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index d0752699..3891feab 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -1,15 +1,13 @@ import EventOrganizerMail from "./emails/EventOrganizerMail"; import EventAttendeeMail from "./emails/EventAttendeeMail"; -import { v5 as uuidv5 } from "uuid"; -import short from "short-uuid"; import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; - -const translator = short(); +import prisma from "./prisma"; +import { Credential } from "@prisma/client"; +import CalEventParser from "./CalEventParser"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { google } = require("googleapis"); -import prisma from "./prisma"; const googleAuth = (credential) => { const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; @@ -124,7 +122,7 @@ export interface CalendarEvent { } export interface ConferenceData { - createRequest: any; + createRequest: unknown; } export interface IntegrationCalendar { @@ -135,13 +133,13 @@ export interface IntegrationCalendar { } export interface CalendarApiAdapter { - createEvent(event: CalendarEvent): Promise; + 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; } @@ -507,10 +505,12 @@ const listCalendars = (withCredentials) => results.reduce((acc, calendars) => acc.concat(calendars), []) ); -const createEvent = async (credential, calEvent: CalendarEvent): Promise => { - const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); +const createEvent = async (credential: 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(calEvent) : null; + const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; const maybeHangoutLink = creationResult?.hangoutLink; const maybeEntryPoints = creationResult?.entryPoints; @@ -548,11 +548,17 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise => }; }; -const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEvent): Promise => { - const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); +const updateEvent = async ( + credential: Credential, + uidToUpdate: string, + calEvent: CalendarEvent +): Promise => { + const parser: CalEventParser = new CalEventParser(calEvent); + const newUid: string = parser.getUid(); + const richEvent: CalendarEvent = parser.asRichEvent(); const updateResult = credential - ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) + ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) : null; const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); @@ -577,7 +583,7 @@ const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEv }; }; -const deleteEvent = (credential, uid: string): Promise => { +const deleteEvent = (credential: Credential, uid: string): Promise => { if (credential) { return calendars([credential])[0].deleteEvent(uid); } @@ -585,4 +591,12 @@ const deleteEvent = (credential, uid: string): Promise => { return Promise.resolve({}); }; -export { getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, listCalendars }; +export { + getBusyCalendarTimes, + createEvent, + updateEvent, + deleteEvent, + CalendarEvent, + listCalendars, + IntegrationCalendar, +}; diff --git a/lib/emails/EventAttendeeMail.ts b/lib/emails/EventAttendeeMail.ts index 4a1b3d10..3fd73f17 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,7 +23,7 @@ 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.
@@ -76,7 +77,7 @@ export default class EventAttendeeMail extends EventMail { * * @protected */ - protected getNodeMailerPayload() { + protected getNodeMailerPayload(): Record { return { to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, diff --git a/lib/emails/EventAttendeeRescheduledMail.ts b/lib/emails/EventAttendeeRescheduledMail.ts index 760aa040..1286c7ab 100644 --- a/lib/emails/EventAttendeeRescheduledMail.ts +++ b/lib/emails/EventAttendeeRescheduledMail.ts @@ -7,15 +7,21 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.attendees[0].name},

- Your ${this.calEvent.type} with ${this.calEvent.organizer.name} has been rescheduled to ${this.getInviteeStart().format('h:mma')} - (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')}.
- ` + this.getAdditionalFooter() + ` + Your ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } has been rescheduled to ${this.getInviteeStart().format("h:mma")} + (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format("dddd, LL")}.
+ ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -23,12 +29,14 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { 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: `Rescheduled: ${this.calEvent.type} with ${this.calEvent.organizer.name} on ${this.getInviteeStart().format('dddd, LL')}`, + subject: `Rescheduled: ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } on ${this.getInviteeStart().format("dddd, LL")}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -37,4 +45,4 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { protected printNodeMailerError(error: string): void { console.error("SEND_RESCHEDULE_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error); } -} \ No newline at end of file +} diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts index f7194d12..90cee0b5 100644 --- a/lib/emails/EventMail.ts +++ b/lib/emails/EventMail.ts @@ -1,3 +1,5 @@ +import CalEventParser from "../CalEventParser"; +import { stripHtml } from "./helpers"; import { CalendarEvent, ConferenceData } from "../calendarClient"; import { serverConfig } from "../serverConfig"; import nodemailer from "nodemailer"; @@ -21,6 +23,7 @@ interface AdditionInformation { export default abstract class EventMail { calEvent: CalendarEvent; + parser: CalEventParser; uid: string; additionInformation?: AdditionInformation; @@ -35,6 +38,7 @@ export default abstract class EventMail { constructor(calEvent: CalendarEvent, uid: string, additionInformation: AdditionInformation = null) { this.calEvent = calEvent; this.uid = uid; + this.parser = new CalEventParser(calEvent); this.additionInformation = additionInformation; } @@ -52,24 +56,14 @@ export default abstract class EventMail { * @protected */ protected getPlainTextRepresentation(): string { - return this.stripHtml(this.getHtmlRepresentation()); - } - - /** - * Strips off all HTML tags and leaves plain text. - * - * @param html - * @protected - */ - protected stripHtml(html: string): string { - return html.replace("
", "\n").replace(/<[^>]+>/g, ""); + return stripHtml(this.getHtmlRepresentation()); } /** * Returns the payload object for the nodemailer. * @protected */ - protected abstract getNodeMailerPayload(); + protected abstract getNodeMailerPayload(): Record; /** * Sends the email to the event attendant and returns a Promise. @@ -129,7 +123,7 @@ export default abstract class EventMail { * @protected */ protected getRescheduleLink(): string { - return process.env.BASE_URL + "/reschedule/" + this.uid; + return this.parser.getRescheduleLink(); } /** @@ -138,7 +132,7 @@ export default abstract class EventMail { * @protected */ protected getCancelLink(): string { - return process.env.BASE_URL + "/cancel/" + this.uid; + return this.parser.getCancelLink(); } /** @@ -146,12 +140,6 @@ export default abstract class EventMail { * @protected */ protected getAdditionalFooter(): string { - return ` -
-
- Need to change this event?
- Cancel: ${this.getCancelLink()}
- Reschedule: ${this.getRescheduleLink()} - `; + return this.parser.getChangeEventFooterHtml(); } } diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index cc429ed1..48b5f078 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -6,6 +6,8 @@ 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 { stripHtml } from "./helpers"; + dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(toArray); @@ -28,11 +30,11 @@ export default class EventOrganizerMail extends EventMail { title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`, description: this.calEvent.description + - this.stripHtml(this.getAdditionalBody()) + - this.stripHtml(this.getAdditionalFooter()), + stripHtml(this.getAdditionalBody()) + + 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, })), @@ -72,7 +74,7 @@ export default class EventOrganizerMail extends EventMail { ${this.calEvent.description} ` + this.getAdditionalFooter() + - ` + ` ` ); @@ -114,7 +116,7 @@ export default class EventOrganizerMail extends EventMail { * * @protected */ - protected getNodeMailerPayload() { + protected getNodeMailerPayload(): Record { const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); return { diff --git a/lib/emails/EventOrganizerRescheduledMail.ts b/lib/emails/EventOrganizerRescheduledMail.ts index 7e67ac44..af9cc864 100644 --- a/lib/emails/EventOrganizerRescheduledMail.ts +++ b/lib/emails/EventOrganizerRescheduledMail.ts @@ -1,4 +1,4 @@ -import dayjs, {Dayjs} from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import EventOrganizerMail from "./EventOrganizerMail"; export default class EventOrganizerRescheduledMail extends EventOrganizerMail { @@ -8,7 +8,8 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.organizer.name},

@@ -19,22 +20,26 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail {
Invitee Email:
${this.calEvent.attendees[0].email}
-
` + this.getAdditionalBody() + - ( - this.calEvent.location ? ` +
` + + this.getAdditionalBody() + + (this.calEvent.location + ? ` Location:
${this.calEvent.location}

- ` : '' - ) + + ` + : "") + `Invitee Time Zone:
${this.calEvent.attendees[0].timeZone}

Additional notes:
${this.calEvent.description} - ` + this.getAdditionalFooter() + ` + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -42,17 +47,19 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); return { icalEvent: { - filename: 'event.ics', + filename: "event.ics", content: this.getiCalEventAsString(), }, from: `Calendso <${this.getMailerOptions().from}>`, to: this.calEvent.organizer.email, - subject: `Rescheduled event: ${this.calEvent.attendees[0].name} - ${organizerStart.format('LT dddd, LL')} - ${this.calEvent.type}`, + subject: `Rescheduled event: ${this.calEvent.attendees[0].name} - ${organizerStart.format( + "LT dddd, LL" + )} - ${this.calEvent.type}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -61,4 +68,4 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { protected printNodeMailerError(error: string): void { console.error("SEND_RESCHEDULE_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); } -} \ No newline at end of file +} diff --git a/lib/emails/VideoEventOrganizerMail.ts b/lib/emails/VideoEventOrganizerMail.ts index 60d85237..1f5b2384 100644 --- a/lib/emails/VideoEventOrganizerMail.ts +++ b/lib/emails/VideoEventOrganizerMail.ts @@ -1,7 +1,7 @@ -import {CalendarEvent} from "../calendarClient"; +import { CalendarEvent } from "../calendarClient"; import EventOrganizerMail from "./EventOrganizerMail"; -import {VideoCallData} from "../videoClient"; -import {getFormattedMeetingId, getIntegrationName} from "./helpers"; +import { VideoCallData } from "../videoClient"; +import { getFormattedMeetingId, getIntegrationName } from "./helpers"; export default class VideoEventOrganizerMail extends EventOrganizerMail { videoCallData: VideoCallData; @@ -18,11 +18,12 @@ export default class VideoEventOrganizerMail extends EventOrganizerMail { * @protected */ protected getAdditionalBody(): string { + // This odd indentation is necessary because otherwise the leading tabs will be applied into the event description. return ` - Video call provider: ${getIntegrationName(this.videoCallData)}
- Meeting ID: ${getFormattedMeetingId(this.videoCallData)}
- Meeting Password: ${this.videoCallData.password}
- Meeting URL: ${this.videoCallData.url}
+Video call provider: ${getIntegrationName(this.videoCallData)}
+Meeting ID: ${getFormattedMeetingId(this.videoCallData)}
+Meeting Password: ${this.videoCallData.password}
+Meeting URL: ${this.videoCallData.url}
`; } -} \ No newline at end of file +} diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index ed5a10c4..e5218a0a 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -1,4 +1,4 @@ -import {VideoCallData} from "../videoClient"; +import { VideoCallData } from "../videoClient"; export function getIntegrationName(videoCallData: VideoCallData): string { //TODO: When there are more complex integration type strings, we should consider using an extra field in the DB for that. @@ -6,15 +6,24 @@ export function getIntegrationName(videoCallData: VideoCallData): string { return nameProto.charAt(0).toUpperCase() + nameProto.slice(1); } +function extractZoom(videoCallData: VideoCallData): string { + const strId = videoCallData.id.toString(); + const part1 = strId.slice(0, 3); + const part2 = strId.slice(3, 7); + const part3 = strId.slice(7, 11); + + return part1 + " " + part2 + " " + part3; +} + export function getFormattedMeetingId(videoCallData: VideoCallData): string { - switch(videoCallData.type) { - case 'zoom_video': - const strId = videoCallData.id.toString(); - const part1 = strId.slice(0, 3); - const part2 = strId.slice(3, 7); - const part3 = strId.slice(7, 11); - return part1 + " " + part2 + " " + part3; + switch (videoCallData.type) { + case "zoom_video": + return extractZoom(videoCallData); default: return videoCallData.id.toString(); } -} \ No newline at end of file +} + +export function stripHtml(html: string): string { + return html.replace("
", "\n").replace(/<[^>]+>/g, ""); +} diff --git a/lib/slots.ts b/lib/slots.ts index 5beeb9f8..3313fbce 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -1,6 +1,7 @@ import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; + dayjs.extend(utc); dayjs.extend(timezone); diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 7f81af14..47c40dc5 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { GetServerSideProps } from "next"; import Head from "next/head"; -import { ClockIcon, GlobeIcon, ChevronDownIcon } from "@heroicons/react/solid"; +import { ChevronDownIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid"; import prisma from "../../lib/prisma"; import { useRouter } from "next/router"; import { Dayjs } from "dayjs"; diff --git a/pages/_app.tsx b/pages/_app.tsx index f521f293..5473bd41 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,15 +1,15 @@ -import '../styles/globals.css'; -import {createTelemetryClient, TelemetryProvider} from '../lib/telemetry'; -import { Provider } from 'next-auth/client'; +import "../styles/globals.css"; +import { createTelemetryClient, TelemetryProvider } from "../lib/telemetry"; +import { Provider } from "next-auth/client"; import type { AppProps } from "next/app"; function MyApp({ Component, pageProps }: AppProps) { return ( - - - - - + + + + + ); } diff --git a/pages/api/availability/week.ts b/pages/api/availability/week.ts index d52c55d7..f0cd408e 100644 --- a/pages/api/availability/week.ts +++ b/pages/api/availability/week.ts @@ -1,30 +1,29 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { getSession } from 'next-auth/client'; -import prisma from '../../../lib/prisma'; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getSession } from "next-auth/client"; +import prisma from "../../../lib/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({req: req}); + const session = await getSession({ req: req }); if (!session) { - res.status(401).json({message: "Not authenticated"}); + res.status(401).json({ message: "Not authenticated" }); return; } if (req.method == "PATCH") { - const startMins = req.body.start; const endMins = req.body.end; - const updateWeek = await prisma.schedule.update({ + await prisma.schedule.update({ where: { id: session.user.id, }, data: { startTime: startMins, - endTime: endMins + endTime: endMins, }, }); - res.status(200).json({message: 'Start and end times updated successfully'}); + res.status(200).json({ message: "Start and end times updated successfully" }); } -} \ No newline at end of file +} diff --git a/pages/api/teams.ts b/pages/api/teams.ts index 1c1254fb..73a48bd8 100644 --- a/pages/api/teams.ts +++ b/pages/api/teams.ts @@ -1,18 +1,16 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../lib/prisma'; -import {getSession} from "next-auth/client"; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "../../lib/prisma"; +import { getSession } from "next-auth/client"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - - const session = await getSession({req: req}); + const session = await getSession({ req: req }); if (!session) { - res.status(401).json({message: "Not authenticated"}); + res.status(401).json({ message: "Not authenticated" }); return; } if (req.method === "POST") { - // TODO: Prevent creating a team with identical names? const createTeam = await prisma.team.create({ @@ -25,13 +23,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data: { teamId: createTeam.id, userId: session.user.id, - role: 'OWNER', + role: "OWNER", accepted: true, - } + }, }); - return res.status(201).json({ message: 'Team created' }); + return res.status(201).json({ message: "Team created" }); } - res.status(404).json({ message: 'Team not found' }); + res.status(404).json({ message: "Team not found" }); } diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index 5e99468f..6302ede0 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -10,16 +10,17 @@ import Shell from "@components/Shell"; import { getSession } from "next-auth/client"; import { Scheduler } from "@components/ui/Scheduler"; -import { LocationMarkerIcon, PlusCircleIcon, XIcon, PhoneIcon } from "@heroicons/react/outline"; +import { LocationMarkerIcon, PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline"; import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput"; import { PlusIcon } from "@heroicons/react/solid"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; -dayjs.extend(utc); import timezone from "dayjs/plugin/timezone"; -import { EventType, User, Availability } from "@prisma/client"; +import { Availability, EventType, User } from "@prisma/client"; import { validJson } from "@lib/jsonUtils"; + +dayjs.extend(utc); dayjs.extend(timezone); type Props = { diff --git a/pages/bookings/index.tsx b/pages/bookings/index.tsx index eb62b9d4..adb75ae7 100644 --- a/pages/bookings/index.tsx +++ b/pages/bookings/index.tsx @@ -2,10 +2,9 @@ import Head from "next/head"; import prisma from "../../lib/prisma"; import { getSession, useSession } from "next-auth/client"; import Shell from "../../components/Shell"; -import dayjs from "dayjs"; export default function Bookings({ bookings }) { - const [session, loading] = useSession(); + const [, loading] = useSession(); if (loading) { return

Loading...

; diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index d74c8ddd..c129939f 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,18 +1,16 @@ import { GetServerSideProps } from "next"; -import Head from 'next/head'; -import Shell from '../../components/Shell'; -import SettingsShell from '../../components/Settings'; -import { useEffect, useState } from 'react'; +import Head from "next/head"; +import Shell from "../../components/Shell"; +import SettingsShell from "../../components/Settings"; +import { useEffect, useState } from "react"; import type { Session } from "next-auth"; -import { useSession, getSession } from 'next-auth/client'; -import { - UsersIcon, -} from "@heroicons/react/outline"; +import { getSession, useSession } from "next-auth/client"; +import { UsersIcon } from "@heroicons/react/outline"; import TeamList from "../../components/team/TeamList"; import TeamListItem from "../../components/team/TeamListItem"; export default function Teams() { - const [session, loading] = useSession(); + const [, loading] = useSession(); const [teams, setTeams] = useState([]); const [invites, setInvites] = useState([]); const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); @@ -23,17 +21,17 @@ export default function Teams() { throw new Error(err.message); } return resp.json(); - } + }; const loadData = () => { fetch("/api/user/membership") - .then(handleErrors) - .then((data) => { - setTeams(data.membership.filter((m) => m.role !== "INVITEE")); - setInvites(data.membership.filter((m) => m.role === "INVITEE")); - }) - .catch(console.log); - } + .then(handleErrors) + .then((data) => { + setTeams(data.membership.filter((m) => m.role !== "INVITEE")); + setInvites(data.membership.filter((m) => m.role === "INVITEE")); + }) + .catch(console.log); + }; useEffect(() => { loadData(); @@ -46,17 +44,17 @@ export default function Teams() { const createTeam = (e) => { e.preventDefault(); - return fetch('/api/teams', { - method: 'POST', - body: JSON.stringify({ name: e.target.elements['name'].value }), + return fetch("/api/teams", { + method: "POST", + body: JSON.stringify({ name: e.target.elements["name"].value }), headers: { - 'Content-Type': 'application/json' - } + "Content-Type": "application/json", + }, }).then(() => { loadData(); setShowCreateTeamModal(false); }); - } + }; return ( @@ -73,10 +71,12 @@ export default function Teams() {

View, edit and create teams to organise relationships between users

- {!(invites.length || teams.length) && + {!(invites.length || teams.length) && (
-

Create a team to get started

+

+ Create a team to get started +

Create your first team and invite other users to work together with you.

@@ -84,31 +84,35 @@ export default function Teams() {
- } + )} - {!!(invites.length || teams.length) &&
- -
} + {!!(invites.length || teams.length) && ( +
+ +
+ )}
- {!!teams.length && - - - } + {!!teams.length && } - {!!invites.length &&
-

Open Invitations

-
    - {invites.map((team) => )} -
-
} + {!!invites.length && ( +
+

Open Invitations

+
    + {invites.map((team) => ( + + ))} +
+
+ )}
{/*{teamsLoaded &&
@@ -124,12 +128,20 @@ export default function Teams() {
}*/}
- {showCreateTeamModal && -
+ {showCreateTeamModal && ( +
- + - +
@@ -137,32 +149,44 @@ export default function Teams() {
- +
-

- Create a new team to collaborate with users. -

+

Create a new team to collaborate with users.

- - + +
- + +
- } + )}
); @@ -170,12 +194,12 @@ export default function Teams() { // Export the `session` prop to use sessions with Server Side Rendering export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (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" } }; + } - return { - props: { session } - } -} + return { + props: { session }, + }; +}; diff --git a/test/lib/prisma.test.ts b/test/lib/prisma.test.ts index b2164ffd..464d6d1a 100644 --- a/test/lib/prisma.test.ts +++ b/test/lib/prisma.test.ts @@ -1,5 +1,4 @@ - -import { it, expect } from '@jest/globals'; +import { expect, it } from "@jest/globals"; import { whereAndSelect } from "@lib/prisma"; it("can decorate using whereAndSelect", async () => { diff --git a/test/lib/slots.test.ts b/test/lib/slots.test.ts index 9412b858..580619a4 100644 --- a/test/lib/slots.test.ts +++ b/test/lib/slots.test.ts @@ -1,9 +1,10 @@ -import getSlots from '@lib/slots'; -import {it, expect} from '@jest/globals'; -import MockDate from 'mockdate'; -import dayjs, {Dayjs} from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; +import getSlots from "@lib/slots"; +import { expect, it } from "@jest/globals"; +import MockDate from "mockdate"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; + dayjs.extend(utc); dayjs.extend(timezone); @@ -53,4 +54,4 @@ it('can cut off dates that due to invitee timezone differences fall on the previ ], organizerTimeZone: 'Europe/London' })).toHaveLength(0); -}); \ No newline at end of file +});