add linting in CI + fix lint errors (#473)

* run `yarn lint --fix`

* Revert "Revert "add linting to ci""

This reverts commit 0bbbbee4be.

* Fixed some errors

* remove unused code - not sure why this was here?

* assert env var

* more type fixes

* fix typings og gcal callback - needs testing

* rename `md5.ts` to `md5.js`

it is js.

* fix types

* fix types

* fix lint errors

* fix last lint error

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Alex Johansson 2021-08-19 14:27:01 +02:00 committed by GitHub
parent 5a9961f608
commit f63aa5d550
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 988 additions and 959 deletions

21
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Lint
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
version: 14.x
- name: Install deps
uses: bahmutov/npm-install@v1
- name: Lint
run: yarn lint

View file

@ -1,22 +1,22 @@
import {useRouter} from 'next/router' import { useRouter } from "next/router";
import Link from 'next/link' import Link from "next/link";
import React, {Children} from 'react' import React, { Children } from "react";
const ActiveLink = ({ children, activeClassName, ...props }) => { const ActiveLink = ({ children, activeClassName, ...props }) => {
const { asPath } = useRouter() const { asPath } = useRouter();
const child = Children.only(children) const child = Children.only(children);
const childClassName = child.props.className || '' const childClassName = child.props.className || "";
const className = const className =
asPath === props.href || asPath === props.as asPath === props.href || asPath === props.as
? `${childClassName} ${activeClassName}`.trim() ? `${childClassName} ${activeClassName}`.trim()
: childClassName : childClassName;
return <Link {...props}>{React.cloneElement(child, { className })}</Link>; return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
} };
ActiveLink.defaultProps = { ActiveLink.defaultProps = {
activeClassName: 'active' activeClassName: "active",
} as Partial<Props> } as Partial<Props>;
export default ActiveLink export default ActiveLink;

View file

@ -1,7 +1,12 @@
import { useState } from "react"; import { useState } from "react";
import md5 from '../lib/md5'; import md5 from "../lib/md5";
export default function Avatar({ user, className = '', fallback, imageSrc = '' }: { export default function Avatar({
user,
className = "",
fallback,
imageSrc = "",
}: {
user: any; user: any;
className?: string; className?: string;
fallback?: JSX.Element; fallback?: JSX.Element;

View file

@ -16,8 +16,8 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
const openUploaderModal = () => { const openUploaderModal = () => {
imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false); imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false);
setImageUploadModalOpen(!imageUploadModalOpen) setImageUploadModalOpen(!imageUploadModalOpen);
} };
const closeImageUploadModal = () => { const closeImageUploadModal = () => {
setImageUploadModalOpen(false); setImageUploadModalOpen(false);
@ -32,34 +32,33 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
const readFile = (file) => { const readFile = (file) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result), false); reader.addEventListener("load", () => resolve(reader.result), false);
reader.readAsDataURL(file) reader.readAsDataURL(file);
}) });
} };
const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
setCroppedAreaPixels(croppedAreaPixels); setCroppedAreaPixels(croppedAreaPixels);
}, []);
}, [])
const CropHandler = () => { const CropHandler = () => {
setCrop({ x: 0, y: 0 }); setCrop({ x: 0, y: 0 });
setZoom(1); setZoom(1);
setImageLoaded(true); setImageLoaded(true);
} };
const handleZoomSliderChange = ([value]) => { const handleZoomSliderChange = ([value]) => {
value < 1 ? setZoom(1) : setZoom(value); value < 1 ? setZoom(1) : setZoom(value);
} };
const createImage = (url) => const createImage = (url) =>
new Promise<HTMLImageElement>((resolve, reject) => { new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image(); const image = new Image();
image.addEventListener('load', () => resolve(image)); image.addEventListener("load", () => resolve(image));
image.addEventListener('error', error => reject(error)); image.addEventListener("error", (error) => reject(error));
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url; image.src = url;
}) });
function getRadianAngle(degreeValue) { function getRadianAngle(degreeValue) {
return (degreeValue * Math.PI) / 180; return (degreeValue * Math.PI) / 180;
@ -67,8 +66,8 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) {
const image = await createImage(imageSrc); const image = await createImage(imageSrc);
const canvas = document.createElement('canvas'); const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const maxSize = Math.max(image.width, image.height); const maxSize = Math.max(image.width, image.height);
const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
@ -84,11 +83,7 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
ctx.translate(-safeArea / 2, -safeArea / 2); ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data. // draw rotated image and store data.
ctx.drawImage( ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
image,
safeArea / 2 - image.width * 0.5,
safeArea / 2 - image.height * 0.5
);
const data = ctx.getImageData(0, 0, safeArea, safeArea); const data = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context // set canvas width to final desired crop size - this will clear existing context
@ -103,24 +98,19 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
); );
// As Base64 string // As Base64 string
return canvas.toDataURL('image/jpeg'); return canvas.toDataURL("image/jpeg");
} }
const showCroppedImage = useCallback(async () => { const showCroppedImage = useCallback(async () => {
try { try {
const croppedImage = await getCroppedImg( const croppedImage = await getCroppedImg(imageDataUrl, croppedAreaPixels, rotation);
imageDataUrl, setIsImageShown(true);
croppedAreaPixels, setShownImage(croppedImage);
rotation setImageLoaded(false);
) handleAvatarChange(croppedImage);
setIsImageShown(true) closeImageUploadModal();
setShownImage(croppedImage)
setImageLoaded(false)
handleAvatarChange(croppedImage)
closeImageUploadModal()
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
}, [croppedAreaPixels, rotation]); }, [croppedAreaPixels, rotation]);
@ -129,13 +119,11 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
<button <button
type="button" type="button"
className="ml-4 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;" className="ml-4 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;"
onClick={openUploaderModal} onClick={openUploaderModal}>
>
{buttonMsg} {buttonMsg}
</button> </button>
{ {imageUploadModalOpen && (
imageUploadModalOpen &&
<div <div
className="fixed z-10 inset-0 overflow-y-auto" className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title" aria-labelledby="modal-title"
@ -152,7 +140,6 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-6"> <div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4"> <div className="sm:flex sm:items-start mb-4">
<div className="mt-3 text-center sm:mt-0 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="text-lg leading-6 font-bold text-gray-900" id="modal-title"> <h3 className="text-lg leading-6 font-bold text-gray-900" id="modal-title">
Upload an avatar Upload an avatar
@ -161,21 +148,17 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
</div> </div>
<div className="mb-4"> <div className="mb-4">
<div className="cropper mt-6 flex flex-col justify-center items-center p-8 bg-gray-50"> <div className="cropper mt-6 flex flex-col justify-center items-center p-8 bg-gray-50">
{!imageLoaded && {!imageLoaded && (
<div className="flex justify-start items-center bg-gray-500 max-h-20 h-20 w-20 rounded-full"> <div className="flex justify-start items-center bg-gray-500 max-h-20 h-20 w-20 rounded-full">
{!isImageShown && {!isImageShown && (
<p className="sm:text-xs text-sm text-white w-full text-center">No {target}</p> <p className="sm:text-xs text-sm text-white w-full text-center">No {target}</p>
} )}
{isImageShown && {isImageShown && (
<img <img className="h-20 w-20 rounded-full" src={shownImage} alt={target} />
className="h-20 w-20 rounded-full" )}
src={shownImage}
alt={target}
/>
}
</div> </div>
} )}
{imageLoaded && {imageLoaded && (
<div className="crop-container max-h-40 h-40 w-40 rounded-full"> <div className="crop-container max-h-40 h-40 w-40 rounded-full">
<div className="relative h-40 w-40 rounded-full"> <div className="relative h-40 w-40 rounded-full">
<Cropper <Cropper
@ -197,8 +180,12 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
changeHandler={handleZoomSliderChange} changeHandler={handleZoomSliderChange}
/> />
</div> </div>
} )}
<label htmlFor={id} className="mt-8 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;">Choose a file...</label> <label
htmlFor={id}
className="mt-8 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;">
Choose a file...
</label>
<input <input
onChange={ImageUploadHandler} onChange={ImageUploadHandler}
ref={imageFileRef} ref={imageFileRef}
@ -215,19 +202,14 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange
<button type="button" className="btn btn-primary" onClick={showCroppedImage}> <button type="button" className="btn btn-primary" onClick={showCroppedImage}>
Save Save
</button> </button>
<button <button onClick={closeImageUploadModal} type="button" className="btn btn-white mr-2">
onClick={closeImageUploadModal}
type="button"
className="btn btn-white mr-2">
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)}
}
</div> </div>
);
)
} }

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from "react";
import * as SliderPrimitive from '@radix-ui/react-slider'; import * as SliderPrimitive from "@radix-ui/react-slider";
const Slider = ({ value, min, max, step, label, changeHandler }) => ( const Slider = ({ value, min, max, step, label, changeHandler }) => (
<SliderPrimitive.Root <SliderPrimitive.Root
@ -9,8 +9,7 @@ const Slider = ({value, min, max, step, label, changeHandler}) => (
max={max} max={max}
value={[value]} value={[value]}
aria-label={label} aria-label={label}
onValueChange={changeHandler} onValueChange={changeHandler}>
>
<SliderPrimitive.Track className="slider-track"> <SliderPrimitive.Track className="slider-track">
<SliderPrimitive.Range className="slider-range" /> <SliderPrimitive.Range className="slider-range" />
</SliderPrimitive.Track> </SliderPrimitive.Track>
@ -19,4 +18,3 @@ const Slider = ({value, min, max, step, label, changeHandler}) => (
); );
export default Slider; export default Slider;

View file

@ -1,19 +1,20 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Dropdown(props) { export default function Dropdown(props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
document.addEventListener('keyup', (e) => { document.addEventListener("keyup", (e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
setOpen(false); setOpen(false);
} }
}); });
}, [open]); }, [open]);
return (<div onClick={() => setOpen(!open)} {...props}> return (
<div onClick={() => setOpen(!open)} {...props}>
{props.children[0]} {props.children[0]}
{open && props.children[1]} {open && props.children[1]}
</div>); </div>
);
} }

View file

@ -1,5 +1,4 @@
import { XCircleIcon } from "@heroicons/react/solid";
import { XCircleIcon } from '@heroicons/react/solid'
export default function ErrorAlert(props) { export default function ErrorAlert(props) {
return ( return (
@ -11,12 +10,10 @@ export default function ErrorAlert(props) {
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Something went wrong</h3> <h3 className="text-sm font-medium text-red-800">Something went wrong</h3>
<div className="text-sm text-red-700"> <div className="text-sm text-red-700">
<p> <p>{props.message}</p>
{props.message}
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) );
} }

View file

@ -1,48 +1,51 @@
// handles logic related to user clock display using 24h display / timeZone options. // handles logic related to user clock display using 24h display / timeZone options.
import dayjs, {Dayjs} from 'dayjs'; import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc'; import utc from "dayjs/plugin/utc";
import timezone from 'dayjs/plugin/timezone'; import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc) dayjs.extend(utc);
dayjs.extend(timezone) dayjs.extend(timezone);
interface TimeOptions { is24hClock: boolean, inviteeTimeZone: string }; interface TimeOptions {
is24hClock: boolean;
inviteeTimeZone: string;
}
const timeOptions: TimeOptions = { const timeOptions: TimeOptions = {
is24hClock: false, is24hClock: false,
inviteeTimeZone: '', inviteeTimeZone: "",
} };
const isInitialized: boolean = false; const isInitialized = false;
const initClock = () => { const initClock = () => {
if (typeof localStorage === "undefined" || isInitialized) { if (typeof localStorage === "undefined" || isInitialized) {
return; return;
} }
timeOptions.is24hClock = localStorage.getItem('timeOption.is24hClock') === "true"; timeOptions.is24hClock = localStorage.getItem("timeOption.is24hClock") === "true";
timeOptions.inviteeTimeZone = localStorage.getItem('timeOption.preferredTimeZone') || dayjs.tz.guess(); timeOptions.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
} };
const is24h = (is24hClock?: boolean) => { const is24h = (is24hClock?: boolean) => {
initClock(); initClock();
if (typeof is24hClock !== "undefined") set24hClock(is24hClock); if (typeof is24hClock !== "undefined") set24hClock(is24hClock);
return timeOptions.is24hClock; return timeOptions.is24hClock;
} };
const set24hClock = (is24hClock: boolean) => { const set24hClock = (is24hClock: boolean) => {
localStorage.setItem('timeOption.is24hClock', is24hClock.toString()); localStorage.setItem("timeOption.is24hClock", is24hClock.toString());
timeOptions.is24hClock = is24hClock; timeOptions.is24hClock = is24hClock;
} };
function setTimeZone(selectedTimeZone: string) { function setTimeZone(selectedTimeZone: string) {
localStorage.setItem('timeOption.preferredTimeZone', selectedTimeZone); localStorage.setItem("timeOption.preferredTimeZone", selectedTimeZone);
timeOptions.inviteeTimeZone = selectedTimeZone; timeOptions.inviteeTimeZone = selectedTimeZone;
} }
const timeZone = (selectedTimeZone?: string) => { const timeZone = (selectedTimeZone?: string) => {
initClock(); initClock();
if (selectedTimeZone) setTimeZone(selectedTimeZone) if (selectedTimeZone) setTimeZone(selectedTimeZone);
return timeOptions.inviteeTimeZone; return timeOptions.inviteeTimeZone;
} };
export { is24h, timeZone }; export { is24h, timeZone };

View file

@ -1,6 +1,5 @@
import { serverConfig } from "../serverConfig"; import { serverConfig } from "../serverConfig";
import nodemailer from 'nodemailer'; import nodemailer from "nodemailer";
export default function createInvitationEmail(data: any, options: any = {}) { export default function createInvitationEmail(data: any, options: any = {}) {
return sendEmail(data, { return sendEmail(data, {
@ -8,22 +7,21 @@ export default function createInvitationEmail(data: any, options: any = {}) {
transport: serverConfig.transport, transport: serverConfig.transport,
from: serverConfig.from, from: serverConfig.from,
}, },
...options ...options,
}); });
} }
const sendEmail = (invitation: any, { const sendEmail = (invitation: any, { provider }) =>
provider, new Promise((resolve, reject) => {
}) => new Promise( (resolve, reject) => {
const { transport, from } = provider; const { transport, from } = provider;
nodemailer.createTransport(transport).sendMail( nodemailer.createTransport(transport).sendMail(
{ {
from: `Calendso <${from}>`, from: `Calendso <${from}>`,
to: invitation.toEmail, to: invitation.toEmail,
subject: ( subject:
invitation.from ? invitation.from + ' invited you' : 'You have been invited' (invitation.from ? invitation.from + " invited you" : "You have been invited") +
) + ` to join ${invitation.teamName}`, ` to join ${invitation.teamName}`,
html: html(invitation), html: html(invitation),
text: text(invitation), text: text(invitation),
}, },
@ -33,7 +31,8 @@ const sendEmail = (invitation: any, {
return reject(new Error(error)); return reject(new Error(error));
} }
return resolve(); return resolve();
}); }
);
}); });
const html = (invitation: any) => { const html = (invitation: any) => {
@ -42,7 +41,8 @@ const html = (invitation: any) => {
url = `${process.env.BASE_URL}/auth/signup?token=${invitation.token}&callbackUrl=${url}`; url = `${process.env.BASE_URL}/auth/signup?token=${invitation.token}&callbackUrl=${url}`;
} }
return ` return (
`
<table style="width: 100%;"> <table style="width: 100%;">
<tr> <tr>
<td> <td>
@ -52,8 +52,8 @@ const html = (invitation: any) => {
<td> <td>
Hi,<br /> Hi,<br />
<br />` + <br />` +
(invitation.from ? invitation.from + ' invited you' : 'You have been invited' ) (invitation.from ? invitation.from + " invited you" : "You have been invited") +
+ ` to join the team "${invitation.teamName}" in Calendso.<br /> ` to join the team "${invitation.teamName}" in Calendso.<br />
<br /> <br />
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr> <tr>
@ -79,8 +79,12 @@ const html = (invitation: any) => {
</td> </td>
</tr> </tr>
</table> </table>
`; `
} );
};
// just strip all HTML and convert <br /> to \n // just strip all HTML and convert <br /> to \n
const text = (evt: any) => html(evt).replace('<br />', "\n").replace(/<[^>]+>/g, ''); const text = (evt: any) =>
html(evt)
.replace("<br />", "\n")
.replace(/<[^>]+>/g, "");

View file

@ -1,5 +1,3 @@
export function getEventName(name: string, eventTitle: string, eventNameTemplate?: string) { export function getEventName(name: string, eventTitle: string, eventNameTemplate?: string) {
return eventNameTemplate return eventNameTemplate ? eventNameTemplate.replace("{USER}", name) : eventTitle + " with " + name;
? eventNameTemplate.replace("{USER}", name)
: eventTitle + ' with ' + name
} }

View file

@ -1,4 +1,4 @@
export function getIntegrationName(name: String) { export function getIntegrationName(name: string) {
switch (name) { switch (name) {
case "google_calendar": case "google_calendar":
return "Google Calendar"; return "Google Calendar";
@ -13,9 +13,9 @@ export function getIntegrationName(name: String) {
} }
} }
export function getIntegrationType(name: String) { export function getIntegrationType(name: string) {
if (name.endsWith('_calendar')) { if (name.endsWith("_calendar")) {
return 'Calendar'; return "Calendar";
} }
return "Unknown"; return "Unknown";
} }

View file

@ -1,5 +1,5 @@
function md5cycle(x, k) { function md5cycle(x, k) {
var a = x[0], let a = x[0],
b = x[1], b = x[1],
c = x[2], c = x[2],
d = x[3]; d = x[3];
@ -100,17 +100,15 @@ function ii(a, b, c, d, x, s, t) {
} }
function md51(s) { function md51(s) {
let txt = ""; let n = s.length,
var n = s.length,
state = [1732584193, -271733879, -1732584194, 271733878], state = [1732584193, -271733879, -1732584194, 271733878],
i; i;
for (i = 64; i <= s.length; i += 64) { for (i = 64; i <= s.length; i += 64) {
md5cycle(state, md5blk(s.substring(i - 64, i))); md5cycle(state, md5blk(s.substring(i - 64, i)));
} }
s = s.substring(i - 64); s = s.substring(i - 64);
var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (i = 0; i < s.length; i++) for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
tail[i >> 2] |= 0x80 << (i % 4 << 3); tail[i >> 2] |= 0x80 << (i % 4 << 3);
if (i > 55) { if (i > 55) {
md5cycle(state, tail); md5cycle(state, tail);
@ -138,7 +136,7 @@ function md51(s) {
*/ */
function md5blk(s) { function md5blk(s) {
/* I figured global was faster. */ /* I figured global was faster. */
var md5blks = [], let md5blks = [],
i; /* Andy King said do it this way. */ i; /* Andy King said do it this way. */
for (i = 0; i < 64; i += 4) { for (i = 0; i < 64; i += 4) {
md5blks[i >> 2] = md5blks[i >> 2] =
@ -150,18 +148,17 @@ function md5blk(s) {
return md5blks; return md5blks;
} }
var hex_chr = "0123456789abcdef".split(""); const hex_chr = "0123456789abcdef".split("");
function rhex(n) { function rhex(n) {
var s = "", let s = "",
j = 0; j = 0;
for (; j < 4; j++) for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f];
s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f];
return s; return s;
} }
function hex(x) { function hex(x) {
for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); for (let i = 0; i < x.length; i++) x[i] = rhex(x[i]);
return x.join(""); return x.join("");
} }

View file

@ -1,6 +1,4 @@
function detectTransport(): string | any { function detectTransport(): string | any {
if (process.env.EMAIL_SERVER) { if (process.env.EMAIL_SERVER) {
return process.env.EMAIL_SERVER; return process.env.EMAIL_SERVER;
} }
@ -14,7 +12,7 @@ function detectTransport(): string | any {
user: process.env.EMAIL_SERVER_USER, user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD, pass: process.env.EMAIL_SERVER_PASSWORD,
}, },
secure: (port === 465), secure: port === 465,
}; };
return transport; return transport;
@ -22,8 +20,8 @@ function detectTransport(): string | any {
return { return {
sendmail: true, sendmail: true,
newline: 'unix', newline: "unix",
path: '/usr/sbin/sendmail' path: "/usr/sbin/sendmail",
}; };
} }

View file

@ -1,4 +1,4 @@
import React, {useContext} from 'react' import React, { useContext } from "react";
import { jitsuClient, JitsuClient } from "@jitsu/sdk-js"; import { jitsuClient, JitsuClient } from "@jitsu/sdk-js";
/** /**
@ -6,12 +6,12 @@ import {jitsuClient, JitsuClient} from "@jitsu/sdk-js";
* to telemetry collection. * to telemetry collection.
*/ */
export const telemetryEventTypes = { export const telemetryEventTypes = {
pageView: 'page_view', pageView: "page_view",
dateSelected: 'date_selected', dateSelected: "date_selected",
timeSelected: 'time_selected', timeSelected: "time_selected",
bookingConfirmed: 'booking_confirmed', bookingConfirmed: "booking_confirmed",
bookingCancelled: 'booking_cancelled' bookingCancelled: "booking_cancelled",
} };
/** /**
* Telemetry client * Telemetry client
@ -23,10 +23,14 @@ export type TelemetryClient = {
* ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen, * ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen,
* which is handled in Next.js with a popup. * which is handled in Next.js with a popup.
*/ */
withJitsu: (callback: (jitsu: JitsuClient) => void | Promise<void>) => void withJitsu: (callback: (jitsu: JitsuClient) => void | Promise<void>) => void;
} };
const emptyClient: TelemetryClient = {withJitsu: () => {}}; const emptyClient: TelemetryClient = {
withJitsu: () => {
// empty
},
};
function useTelemetry(): TelemetryClient { function useTelemetry(): TelemetryClient {
return useContext(TelemetryContext); return useContext(TelemetryContext);
@ -41,10 +45,10 @@ function isLocalhost(host: string) {
* @param route current next.js route * @param route current next.js route
*/ */
export function collectPageParameters(route?: string): any { export function collectPageParameters(route?: string): any {
let host = document.location.hostname; const host = document.location.hostname;
let maskedHost = isLocalhost(host) ? "localhost" : "masked"; const maskedHost = isLocalhost(host) ? "localhost" : "masked";
//starts with '' //starts with ''
let docPath = route ?? ""; const docPath = route ?? "";
return { return {
page_url: route, page_url: route,
page_title: "", page_title: "",
@ -54,7 +58,7 @@ export function collectPageParameters(route?: string): any {
doc_search: "", doc_search: "",
doc_path: docPath, doc_path: docPath,
referer: "", referer: "",
} };
} }
function createTelemetryClient(): TelemetryClient { function createTelemetryClient(): TelemetryClient {
@ -68,31 +72,30 @@ function createTelemetryClient(): TelemetryClient {
if (!window) { if (!window) {
console.warn("Jitsu has been called during SSR, this scenario isn't supported yet"); console.warn("Jitsu has been called during SSR, this scenario isn't supported yet");
return; return;
} else if (!window['jitsu']) { } else if (!window["jitsu"]) {
window['jitsu'] = jitsuClient({ window["jitsu"] = jitsuClient({
log_level: 'ERROR', log_level: "ERROR",
tracking_host: "https://t.calendso.com", tracking_host: "https://t.calendso.com",
key: process.env.NEXT_PUBLIC_TELEMETRY_KEY, key: process.env.NEXT_PUBLIC_TELEMETRY_KEY,
cookie_name: "__clnds", cookie_name: "__clnds",
capture_3rd_party_cookies: false, capture_3rd_party_cookies: false,
}); });
} }
let res = callback(window['jitsu']); const res = callback(window["jitsu"]);
if (res && typeof res['catch'] === "function") { if (res && typeof res["catch"] === "function") {
res.catch(e => { res.catch((e) => {
console.debug("Unable to send telemetry event", e) console.debug("Unable to send telemetry event", e);
}); });
} }
} },
} };
} else { } else {
return emptyClient; return emptyClient;
} }
} }
const TelemetryContext = React.createContext<TelemetryClient>(emptyClient);
const TelemetryContext = React.createContext<TelemetryClient>(emptyClient) const TelemetryProvider = TelemetryContext.Provider;
const TelemetryProvider = TelemetryContext.Provider
export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry }; export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry };

View file

@ -17,11 +17,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
select: { select: {
id: true, id: true,
password: true password: true,
} },
}); });
if (!user) { res.status(404).json({message: 'User not found'}); return; } if (!user) {
res.status(404).json({ message: "User not found" });
return;
}
const oldPassword = req.body.oldPassword; const oldPassword = req.body.oldPassword;
const newPassword = req.body.newPassword; const newPassword = req.body.newPassword;
@ -29,11 +32,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const passwordsMatch = await verifyPassword(oldPassword, currentPassword); const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
if (!passwordsMatch) { res.status(403).json({message: 'Incorrect password'}); return; } if (!passwordsMatch) {
res.status(403).json({ message: "Incorrect password" });
return;
}
const hashedPassword = await hashPassword(newPassword); const hashedPassword = await hashPassword(newPassword);
const updateUser = await prisma.user.update({ await prisma.user.update({
where: { where: {
id: user.id, id: user.id,
}, },
@ -42,5 +48,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
}); });
res.status(200).json({message: 'Password updated successfully'}); res.status(200).json({ message: "Password updated successfully" });
} }

View file

@ -1,8 +1,8 @@
import prisma from '../../../lib/prisma'; import prisma from "../../../lib/prisma";
import { hashPassword } from "../../../lib/auth"; import { hashPassword } from "../../../lib/auth";
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method !== 'POST') { if (req.method !== "POST") {
return; return;
} }
@ -10,17 +10,17 @@ export default async function handler(req, res) {
const { username, email, password } = data; const { username, email, password } = data;
if (!username) { if (!username) {
res.status(422).json({message: 'Invalid username'}); res.status(422).json({ message: "Invalid username" });
return; return;
} }
if (!email || !email.includes('@')) { if (!email || !email.includes("@")) {
res.status(422).json({message: 'Invalid email'}); res.status(422).json({ message: "Invalid email" });
return; return;
} }
if (!password || password.trim().length < 7) { if (!password || password.trim().length < 7) {
res.status(422).json({message: 'Invalid input - password should be at least 7 characters long.'}); res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." });
return; return;
} }
@ -28,34 +28,33 @@ export default async function handler(req, res) {
where: { where: {
OR: [ OR: [
{ {
username: username username: username,
}, },
{ {
email: email email: email,
} },
], ],
AND: [ AND: [
{ {
emailVerified: { emailVerified: {
not: null, not: null,
}, },
} },
] ],
} },
}); });
if (existingUser) { if (existingUser) {
let message: string = ( const message: string =
existingUser.email !== email existingUser.email !== email ? "Username already taken" : "Email address is already registered";
) ? 'Username already taken' : 'Email address is already registered';
return res.status(409).json({ message }); return res.status(409).json({ message });
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
const user = await prisma.user.upsert({ await prisma.user.upsert({
where: { email, }, where: { email },
update: { update: {
username, username,
password: hashedPassword, password: hashedPassword,
@ -65,8 +64,8 @@ export default async function handler(req, res) {
username, username,
email, email,
password: hashedPassword, password: hashedPassword,
} },
}); });
res.status(201).json({message: 'Created user'}); res.status(201).json({ message: "Created user" });
} }

View file

@ -18,8 +18,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
select: { select: {
credentials: true, credentials: true,
timeZone: true, timeZone: true,
id: true id: true,
} },
}); });
if (req.method == "POST") { if (req.method == "POST") {
@ -27,15 +27,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
data: { data: {
user: { user: {
connect: { connect: {
id: currentUser.id id: currentUser.id,
} },
}, },
integration: req.body.integration, integration: req.body.integration,
externalId: req.body.externalId externalId: req.body.externalId,
} },
}); });
res.status(200).json({ message: "Calendar Selection Saved" }); res.status(200).json({ message: "Calendar Selection Saved" });
} }
if (req.method == "DELETE") { if (req.method == "DELETE") {
@ -44,9 +43,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId_integration_externalId: { userId_integration_externalId: {
userId: currentUser.id, userId: currentUser.id,
externalId: req.body.externalId, externalId: req.body.externalId,
integration: req.body.integration integration: req.body.integration,
} },
} },
}); });
res.status(200).json({ message: "Calendar Selection Saved" }); res.status(200).json({ message: "Calendar Selection Saved" });
@ -55,15 +54,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (req.method == "GET") { if (req.method == "GET") {
const selectedCalendarIds = await prisma.selectedCalendar.findMany({ const selectedCalendarIds = await prisma.selectedCalendar.findMany({
where: { where: {
userId: currentUser.id userId: currentUser.id,
}, },
select: { select: {
externalId: true externalId: true,
} },
}); });
const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials); const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials);
const selectableCalendars = calendars.map(cal => {return {selected: selectedCalendarIds.findIndex(s => s.externalId === cal.externalId) > -1, ...cal}}); const selectableCalendars = calendars.map((cal) => {
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
});
res.status(200).json(selectableCalendars); res.status(200).json(selectableCalendars);
} }
} }

View file

@ -15,17 +15,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const endMins = req.body.end; const endMins = req.body.end;
const bufferMins = req.body.buffer; const bufferMins = req.body.buffer;
const updateDay = await prisma.user.update({ await prisma.user.update({
where: { where: {
id: session.user.id, id: session.user.id,
}, },
data: { data: {
startTime: startMins, startTime: startMins,
endTime: endMins, endTime: endMins,
bufferTime: bufferMins bufferTime: bufferMins,
}, },
}); });
res.status(200).json({message: 'Start and end times updated successfully'}); res.status(200).json({ message: "Start and end times updated successfully" });
} }
} }

View file

@ -2,11 +2,14 @@ import prisma from "../../lib/prisma";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method === 'GET') { if (req.method === "GET") {
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const credentials = await prisma.credential.findMany({ const credentials = await prisma.credential.findMany({
where: { where: {
@ -14,8 +17,8 @@ export default async function handler(req, res) {
}, },
select: { select: {
type: true, type: true,
key: true key: true,
} },
}); });
res.status(200).json(credentials); res.status(200).json(credentials);
@ -24,16 +27,19 @@ export default async function handler(req, res) {
if (req.method == "DELETE") { if (req.method == "DELETE") {
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const id = req.body.id; const id = req.body.id;
const deleteIntegration = await prisma.credential.delete({ await prisma.credential.delete({
where: { where: {
id: id, id: id,
}, },
}); });
res.status(200).json({message: 'Integration deleted successfully'}); res.status(200).json({ message: "Integration deleted successfully" });
} }
} }

View file

@ -1,40 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma"; import { google } from "googleapis";
const { google } = require("googleapis"); import type { NextApiRequest, NextApiResponse } from "next";
const credentials = process.env.GOOGLE_API_CREDENTIALS; const credentials = process.env.GOOGLE_API_CREDENTIALS!;
const scopes = ['https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/calendar.events']; const scopes = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events",
];
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') { if (req.method === "GET") {
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
// Get user return;
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true
} }
});
// Get token from Google Calendar API // Get token from Google Calendar API
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
const authUrl = oAuth2Client.generateAuthUrl({ const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline', access_type: "offline",
scope: scopes, scope: scopes,
// A refresh token is only returned the first time the user // A refresh token is only returned the first time the user
// consents to providing access. For illustration purposes, // consents to providing access. For illustration purposes,
// setting the prompt to 'consent' will force this consent // setting the prompt to 'consent' will force this consent
// every time, forcing a refresh_token to be returned. // every time, forcing a refresh_token to be returned.
prompt: 'consent', prompt: "consent",
}); });
res.status(200).json({ url: authUrl }); res.status(200).json({ url: authUrl });

View file

@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma"; import prisma from "../../../../lib/prisma";
const { google } = require("googleapis"); import { google } from "googleapis";
const credentials = process.env.GOOGLE_API_CREDENTIALS; const credentials = process.env.GOOGLE_API_CREDENTIALS!;
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query; const { code } = req.query;
@ -11,24 +11,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
if (typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
const token = await oAuth2Client.getToken(code);
// Convert to token await prisma.credential.create({
return new Promise( (resolve, reject) => oAuth2Client.getToken(code, async (err, token) => {
if (err) return console.error('Error retrieving access token', err);
const credential = await prisma.credential.create({
data: { data: {
type: 'google_calendar', type: "google_calendar",
key: token, key: token as any,
userId: session.user.id userId: session.user.id,
} },
}); });
res.redirect('/integrations'); res.redirect("/integrations");
resolve();
}));
} }

View file

@ -2,29 +2,40 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma"; import prisma from "../../../../lib/prisma";
const scopes = ['User.Read', 'Calendars.Read', 'Calendars.ReadWrite', 'offline_access']; const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
function generateAuthUrl() {
return (
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" +
scopes.join(" ") +
"&client_id=" +
process.env.MS_GRAPH_CLIENT_ID +
"&redirect_uri=" +
process.env.BASE_URL +
"/api/integrations/office365calendar/callback"
);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') { if (req.method === "GET") {
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
// Get user // Get user
const user = await prisma.user.findFirst({ await prisma.user.findFirst({
where: { where: {
email: session.user.email, email: session.user.email,
}, },
select: { select: {
id: true id: true,
} },
}); });
function generateAuthUrl() {
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=' + scopes.join(' ') + '&client_id=' + process.env.MS_GRAPH_CLIENT_ID + '&redirect_uri=' + process.env.BASE_URL + '/api/integrations/office365calendar/callback';
}
res.status(200).json({ url: generateAuthUrl() }); res.status(200).json({ url: generateAuthUrl() });
} }
} }

View file

@ -8,37 +8,56 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const toUrlEncoded = payload => Object.keys(payload).map( (key) => key + '=' + encodeURIComponent(payload[ key ]) ).join('&'); const toUrlEncoded = (payload) =>
Object.keys(payload)
.map((key) => key + "=" + encodeURIComponent(payload[key]))
.join("&");
const body = toUrlEncoded({ client_id: process.env.MS_GRAPH_CLIENT_ID, grant_type: 'authorization_code', code, scope: scopes.join(' '), redirect_uri: process.env.BASE_URL + '/api/integrations/office365calendar/callback', client_secret: process.env.MS_GRAPH_CLIENT_SECRET }); const body = toUrlEncoded({
client_id: process.env.MS_GRAPH_CLIENT_ID,
grant_type: "authorization_code",
code,
scope: scopes.join(" "),
redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback",
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
});
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: { const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', method: "POST",
}, body }); headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
body,
});
const responseBody = await response.json(); const responseBody = await response.json();
if (!response.ok) { if (!response.ok) {
return res.redirect('/integrations?error=' + JSON.stringify(responseBody)); return res.redirect("/integrations?error=" + JSON.stringify(responseBody));
} }
const whoami = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': 'Bearer ' + responseBody.access_token } }); const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: { Authorization: "Bearer " + responseBody.access_token },
});
const graphUser = await whoami.json(); const graphUser = await whoami.json();
// In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address. // In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address.
responseBody.email = graphUser.mail ?? graphUser.userPrincipalName; responseBody.email = graphUser.mail ?? graphUser.userPrincipalName;
responseBody.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); // set expiry date in seconds responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
delete responseBody.expires_in; delete responseBody.expires_in;
const credential = await prisma.credential.create({ await prisma.credential.create({
data: { data: {
type: 'office365_calendar', type: "office365_calendar",
key: responseBody, key: responseBody,
userId: session.user.id userId: session.user.id,
} },
}); });
return res.redirect('/integrations'); return res.redirect("/integrations");
} }

View file

@ -5,24 +5,31 @@ import prisma from "../../../../lib/prisma";
const client_id = process.env.ZOOM_CLIENT_ID; const client_id = process.env.ZOOM_CLIENT_ID;
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') { if (req.method === "GET") {
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
// Get user // Get user
const user = await prisma.user.findFirst({ await prisma.user.findFirst({
where: { where: {
email: session.user.email, email: session.user.email,
}, },
select: { select: {
id: true id: true,
} },
}); });
const redirectUri = encodeURI(process.env.BASE_URL + '/api/integrations/zoomvideo/callback'); const redirectUri = encodeURI(process.env.BASE_URL + "/api/integrations/zoomvideo/callback");
const authUrl = 'https://zoom.us/oauth/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirectUri; const authUrl =
"https://zoom.us/oauth/authorize?response_type=code&client_id=" +
client_id +
"&redirect_uri=" +
redirectUri;
res.status(200).json({ url: authUrl }); res.status(200).json({ url: authUrl });
} }

View file

@ -1,4 +1,4 @@
import type {NextApiRequest, NextApiResponse} from 'next'; import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/client"; import { getSession } from "next-auth/client";
import prisma from "../../../../lib/prisma"; import prisma from "../../../../lib/prisma";
@ -11,29 +11,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
const redirectUri = encodeURI(process.env.BASE_URL + '/api/integrations/zoomvideo/callback'); return;
const authHeader = 'Basic ' + Buffer.from(client_id + ':' + client_secret).toString('base64');
return new Promise( async (resolve, reject) => {
const result = await fetch('https://zoom.us/oauth/token?grant_type=authorization_code&code=' + code + '&redirect_uri=' + redirectUri, {
method: 'POST',
headers: {
Authorization: authHeader
} }
})
.then(res => res.json()); const redirectUri = encodeURI(process.env.BASE_URL + "/api/integrations/zoomvideo/callback");
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64");
const result = await fetch(
"https://zoom.us/oauth/token?grant_type=authorization_code&code=" + code + "&redirect_uri=" + redirectUri,
{
method: "POST",
headers: {
Authorization: authHeader,
},
}
);
const json = await result.json();
await prisma.credential.create({ await prisma.credential.create({
data: { data: {
type: 'zoom_video', type: "zoom_video",
key: result, key: json,
userId: session.user.id userId: session.user.id,
} },
});
res.redirect('/integrations');
resolve();
}); });
res.redirect("/integrations");
} }

View file

@ -1,9 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from "next";
import prisma from '../../../../lib/prisma'; import prisma from "../../../../lib/prisma";
import { getSession } from "next-auth/client"; import { getSession } from "next-auth/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { if (!session) {
return res.status(401).json({ message: "Not authenticated" }); return res.status(401).json({ message: "Not authenticated" });
@ -11,12 +10,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// DELETE /api/teams/{team} // DELETE /api/teams/{team}
if (req.method === "DELETE") { if (req.method === "DELETE") {
const deleteMembership = await prisma.membership.delete({ await prisma.membership.delete({
where: { where: {
userId_teamId: { userId: session.user.id, teamId: parseInt(req.query.team) } userId_teamId: { userId: session.user.id, teamId: parseInt(req.query.team) },
} },
}); });
const deleteTeam = await prisma.team.delete({ await prisma.team.delete({
where: { where: {
id: parseInt(req.query.team), id: parseInt(req.query.team),
}, },

View file

@ -1,12 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from "next";
import prisma from '../../../../lib/prisma'; import prisma from "../../../../lib/prisma";
import createInvitationEmail from "../../../../lib/emails/invitation"; import createInvitationEmail from "../../../../lib/emails/invitation";
import { getSession } from "next-auth/client"; import { getSession } from "next-auth/client";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import {create} from "domain";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") { if (req.method !== "POST") {
return res.status(400).json({ message: "Bad request" }); return res.status(400).json({ message: "Bad request" });
} }
@ -18,8 +16,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const team = await prisma.team.findFirst({ const team = await prisma.team.findFirst({
where: { where: {
id: parseInt(req.query.team) id: parseInt(req.query.team),
} },
}); });
if (!team) { if (!team) {
@ -28,11 +26,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const invitee = await prisma.user.findFirst({ const invitee = await prisma.user.findFirst({
where: { where: {
OR: [ OR: [{ username: req.body.usernameOrEmail }, { email: req.body.usernameOrEmail }],
{ username: req.body.usernameOrEmail }, },
{ email: req.body.usernameOrEmail }
]
}
}); });
if (!invitee) { if (!invitee) {
@ -41,33 +36,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!isEmail(req.body.usernameOrEmail)) { if (!isEmail(req.body.usernameOrEmail)) {
return res.status(400).json({ return res.status(400).json({
message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}` message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}`,
}); });
} }
// valid email given, create User // valid email given, create User
const createUser = await prisma.user.create( await prisma.user
{ .create({
data: { data: {
email: req.body.usernameOrEmail, email: req.body.usernameOrEmail,
} },
}) })
.then( (invitee) => prisma.membership.create( .then((invitee) =>
{ prisma.membership.create({
data: { data: {
teamId: parseInt(req.query.team), teamId: parseInt(req.query.team as string),
userId: invitee.id, userId: invitee.id,
role: req.body.role, role: req.body.role,
}, },
})); })
);
const token: string = randomBytes(32).toString("hex"); const token: string = randomBytes(32).toString("hex");
const createVerificationRequest = await prisma.verificationRequest.create({ await prisma.verificationRequest.create({
data: { data: {
identifier: req.body.usernameOrEmail, identifier: req.body.usernameOrEmail,
token, token,
expires: new Date((new Date()).setHours(168)) // +1 week expires: new Date(new Date().setHours(168)), // +1 week
} },
}); });
createInvitationEmail({ createInvitationEmail({
@ -82,30 +78,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// create provisional membership // create provisional membership
try { try {
const createMembership = await prisma.membership.create({ await prisma.membership.create({
data: { data: {
teamId: parseInt(req.query.team), teamId: parseInt(req.query.team as string),
userId: invitee.id, userId: invitee.id,
role: req.body.role, role: req.body.role,
}, },
}); });
} } catch (err) {
catch (err) { if (err.code === "P2002") {
if (err.code === "P2002") { // unique constraint violation // unique constraint violation
return res.status(409).json({ return res.status(409).json({
message: 'This user is a member of this team / has a pending invitation.', message: "This user is a member of this team / has a pending invitation.",
}); });
} else { } else {
throw err; // rethrow throw err; // rethrow
} }
}; }
// inform user of membership by email // inform user of membership by email
if (req.body.sendEmailInvitation) { if (req.body.sendEmailInvitation) {
createInvitationEmail({ createInvitationEmail({
toEmail: invitee.email, toEmail: invitee.email,
from: session.user.name, from: session.user.name,
teamName: team.name teamName: team.name,
}); });
} }

View file

@ -1,9 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from "next";
import prisma from '../../../../lib/prisma'; import prisma from "../../../../lib/prisma";
import { getSession } from "next-auth/client"; import { getSession } from "next-auth/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req }); const session = await getSession({ req });
if (!session) { if (!session) {
@ -11,13 +10,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return; return;
} }
const isTeamOwner = !!await prisma.membership.findFirst({ const isTeamOwner = !!(await prisma.membership.findFirst({
where: { where: {
userId: session.user.id, userId: session.user.id,
teamId: parseInt(req.query.team), teamId: parseInt(req.query.team as string),
role: 'OWNER' role: "OWNER",
} },
}); }));
if (!isTeamOwner) { if (!isTeamOwner) {
res.status(403).json({ message: "You are not authorized to manage this team" }); res.status(403).json({ message: "You are not authorized to manage this team" });
@ -28,24 +27,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (req.method === "GET") { if (req.method === "GET") {
const memberships = await prisma.membership.findMany({ const memberships = await prisma.membership.findMany({
where: { where: {
teamId: parseInt(req.query.team), teamId: parseInt(req.query.team as string),
} },
}); });
let members = await prisma.user.findMany({ let members = await prisma.user.findMany({
where: { where: {
id: { id: {
in: memberships.map((membership) => membership.userId), in: memberships.map((membership) => membership.userId),
} },
} },
}); });
members = members.map((member) => { members = members.map((member) => {
const membership = memberships.find((membership) => member.id === membership.userId); const membership = memberships.find((membership) => member.id === membership.userId);
return { return {
...member, ...member,
role: membership.accepted ? membership.role : 'INVITEE', role: membership.accepted ? membership.role : "INVITEE",
} };
}); });
return res.status(200).json({ members: members }); return res.status(200).json({ members: members });
@ -53,10 +52,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Cancel a membership (invite) // Cancel a membership (invite)
if (req.method === "DELETE") { if (req.method === "DELETE") {
const memberships = await prisma.membership.delete({ await prisma.membership.delete({
where: { where: {
userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team) }, userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team) },
} },
}); });
return res.status(204).send(null); return res.status(204).send(null);
} }

View file

@ -1,9 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from "next";
import prisma from '../../../lib/prisma'; import prisma from "../../../lib/prisma";
import { getSession } from "next-auth/client"; import { getSession } from "next-auth/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { if (!session) {
return res.status(401).json({ message: "Not authenticated" }); return res.status(401).json({ message: "Not authenticated" });
@ -13,22 +12,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const memberships = await prisma.membership.findMany({ const memberships = await prisma.membership.findMany({
where: { where: {
userId: session.user.id, userId: session.user.id,
} },
}); });
const teams = await prisma.team.findMany({ const teams = await prisma.team.findMany({
where: { where: {
id: { id: {
in: memberships.map(membership => membership.teamId), in: memberships.map((membership) => membership.teamId),
} },
} },
}); });
return res.status(200).json({ return res.status(200).json({
membership: memberships.map((membership) => ({ membership: memberships.map((membership) => ({
role: membership.accepted ? membership.role : 'INVITEE', role: membership.accepted ? membership.role : "INVITEE",
...teams.find(team => team.id === membership.teamId) ...teams.find((team) => team.id === membership.teamId),
})) })),
}); });
} }
@ -38,23 +37,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Leave team or decline membership invite of current user // Leave team or decline membership invite of current user
if (req.method === "DELETE") { if (req.method === "DELETE") {
const memberships = await prisma.membership.delete({ await prisma.membership.delete({
where: { where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId } userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
} },
}); });
return res.status(204).send(null); return res.status(204).send(null);
} }
// Accept team invitation // Accept team invitation
if (req.method === "PATCH") { if (req.method === "PATCH") {
const memberships = await prisma.membership.update({ await prisma.membership.update({
where: { where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId } userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
}, },
data: { data: {
accepted: true accepted: true,
} },
}); });
return res.status(204).send(null); return res.status(204).send(null);

View file

@ -238,7 +238,6 @@ export default function EventTypePage({
}, },
}); });
router.push("/event-types"); router.push("/event-types");
showToast("Event Type updated", "success"); showToast("Event Type updated", "success");
setSuccessModalOpen(true); setSuccessModalOpen(true);
@ -325,7 +324,7 @@ export default function EventTypePage({
name="address" name="address"
id="address" id="address"
required required
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm" className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
defaultValue={locations.find((location) => location.type === LocationType.InPerson)?.address} defaultValue={locations.find((location) => location.type === LocationType.InPerson)?.address}
/> />
</div> </div>
@ -381,26 +380,26 @@ export default function EventTypePage({
name="title" name="title"
id="title" id="title"
required required
className="pl-0 text-xl font-bold text-gray-900 cursor-pointer border-none focus:ring-0 bg-transparent focus:outline-none" className="pl-0 text-xl font-bold text-gray-900 bg-transparent border-none cursor-pointer focus:ring-0 focus:outline-none"
placeholder="Quick Chat" placeholder="Quick Chat"
defaultValue={eventType.title} defaultValue={eventType.title}
/> />
} }
subtitle={eventType.description}> subtitle={eventType.description}>
<div className="block sm:flex"> <div className="block sm:flex">
<div className="w-full sm:w-10/12 mr-2"> <div className="w-full mr-2 sm:w-10/12">
<div className="bg-white rounded-sm border border-neutral-200 -mx-4 sm:mx-0 p-4 sm:p-8"> <div className="p-4 -mx-4 bg-white border rounded-sm border-neutral-200 sm:mx-0 sm:p-8">
<form onSubmit={updateEventTypeHandler} className="space-y-4"> <form onSubmit={updateEventTypeHandler} className="space-y-4">
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label htmlFor="slug" className="text-sm flex font-medium text-neutral-700 mt-0"> <label htmlFor="slug" className="flex mt-0 text-sm font-medium text-neutral-700">
<LinkIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" /> <LinkIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" />
URL URL
</label> </label>
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="flex rounded-sm shadow-sm"> <div className="flex rounded-sm shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm"> <span className="inline-flex items-center px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm">
{typeof location !== "undefined" ? location.hostname : ""}/{user.username}/ {typeof location !== "undefined" ? location.hostname : ""}/{user.username}/
</span> </span>
<input <input
@ -409,33 +408,33 @@ export default function EventTypePage({
name="slug" name="slug"
id="slug" id="slug"
required required
className="flex-1 block w-full focus:ring-primary-500 focus:border-primary-500 min-w-0 rounded-none rounded-r-sm sm:text-sm border-gray-300" className="flex-1 block w-full min-w-0 border-gray-300 rounded-none rounded-r-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
defaultValue={eventType.slug} defaultValue={eventType.slug}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label htmlFor="length" className="text-sm flex font-medium text-neutral-700 mt-0"> <label htmlFor="length" className="flex mt-0 text-sm font-medium text-neutral-700">
<ClockIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" /> <ClockIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" />
Duration Duration
</label> </label>
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="mt-1 relative rounded-sm shadow-sm"> <div className="relative mt-1 rounded-sm shadow-sm">
<input <input
ref={lengthRef} ref={lengthRef}
type="number" type="number"
name="length" name="length"
id="length" id="length"
required required
className="focus:ring-primary-500 focus:border-primary-500 block w-full pl-2 pr-12 sm:text-sm border-gray-300 rounded-sm" className="block w-full pl-2 pr-12 border-gray-300 rounded-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="15" placeholder="15"
defaultValue={eventType.length} defaultValue={eventType.length}
/> />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<span className="text-gray-500 sm:text-sm" id="duration"> <span className="text-gray-500 sm:text-sm" id="duration">
mins mins
</span> </span>
@ -446,9 +445,9 @@ export default function EventTypePage({
<hr /> <hr />
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label htmlFor="location" className="text-sm flex font-medium text-neutral-700 mt-0"> <label htmlFor="location" className="flex mt-0 text-sm font-medium text-neutral-700">
<LocationMarkerIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" /> <LocationMarkerIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" />
Location Location
</label> </label>
@ -463,7 +462,7 @@ export default function EventTypePage({
options={locationOptions} options={locationOptions}
isSearchable="false" isSearchable="false"
classNamePrefix="react-select" classNamePrefix="react-select"
className="react-select-container rounded-sm border border-gray-300 flex-1 block w-full focus:ring-primary-500 focus:border-primary-500 min-w-0 sm:text-sm" className="flex-1 block w-full min-w-0 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
onChange={(e) => openLocationModal(e.value)} onChange={(e) => openLocationModal(e.value)}
/> />
</div> </div>
@ -474,24 +473,24 @@ export default function EventTypePage({
{locations.map((location) => ( {locations.map((location) => (
<li <li
key={location.type} key={location.type}
className="mb-2 p-2 border border-neutral-300 rounded-sm shadow-sm"> className="p-2 mb-2 border rounded-sm shadow-sm border-neutral-300">
<div className="flex justify-between"> <div className="flex justify-between">
{location.type === LocationType.InPerson && ( {location.type === LocationType.InPerson && (
<div className="flex-grow flex items-center"> <div className="flex items-center flex-grow">
<LocationMarkerIcon className="h-6 w-6" /> <LocationMarkerIcon className="w-6 h-6" />
<span className="ml-2 text-sm">{location.address}</span> <span className="ml-2 text-sm">{location.address}</span>
</div> </div>
)} )}
{location.type === LocationType.Phone && ( {location.type === LocationType.Phone && (
<div className="flex-grow flex items-center"> <div className="flex items-center flex-grow">
<PhoneIcon className="h-6 w-6" /> <PhoneIcon className="w-6 h-6" />
<span className="ml-2 text-sm">Phone call</span> <span className="ml-2 text-sm">Phone call</span>
</div> </div>
)} )}
{location.type === LocationType.GoogleMeet && ( {location.type === LocationType.GoogleMeet && (
<div className="flex-grow flex items-center"> <div className="flex items-center flex-grow">
<svg <svg
className="h-6 w-6" className="w-6 h-6"
viewBox="0 0 64 54" viewBox="0 0 64 54"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -520,9 +519,9 @@ export default function EventTypePage({
</div> </div>
)} )}
{location.type === LocationType.Zoom && ( {location.type === LocationType.Zoom && (
<div className="flex-grow flex items-center"> <div className="flex items-center flex-grow">
<svg <svg
className="h-6 w-6" className="w-6 h-6"
viewBox="0 0 64 64" viewBox="0 0 64 64"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -554,7 +553,7 @@ export default function EventTypePage({
Edit Edit
</button> </button>
<button onClick={() => removeLocation(location)}> <button onClick={() => removeLocation(location)}>
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " /> <XIcon className="w-6 h-6 pl-1 border-l-2 hover:text-red-500 " />
</button> </button>
</div> </div>
</div> </div>
@ -564,10 +563,10 @@ export default function EventTypePage({
<li> <li>
<button <button
type="button" type="button"
className="bg-neutral-100 rounded-sm py-2 px-3 flex" className="flex px-3 py-2 rounded-sm bg-neutral-100"
onClick={() => setShowLocationModal(true)}> onClick={() => setShowLocationModal(true)}>
<PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" /> <PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" />
<span className="ml-1 text-neutral-700 text-sm font-medium"> <span className="ml-1 text-sm font-medium text-neutral-700">
Add another location Add another location
</span> </span>
</button> </button>
@ -580,9 +579,9 @@ export default function EventTypePage({
<hr className="border-neutral-200" /> <hr className="border-neutral-200" />
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label htmlFor="description" className="text-sm flex font-medium text-neutral-700 mt-0"> <label htmlFor="description" className="flex mt-0 text-sm font-medium text-neutral-700">
<DocumentIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" /> <DocumentIcon className="w-4 h-4 mr-2 mt-0.5 text-neutral-500" />
Description Description
</label> </label>
@ -592,7 +591,7 @@ export default function EventTypePage({
ref={descriptionRef} ref={descriptionRef}
name="description" name="description"
id="description" id="description"
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm" className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="A quick video meeting." placeholder="A quick video meeting."
defaultValue={eventType.description}></textarea> defaultValue={eventType.description}></textarea>
</div> </div>
@ -600,47 +599,47 @@ export default function EventTypePage({
<Disclosure> <Disclosure>
{({ open }) => ( {({ open }) => (
<> <>
<Disclosure.Button className="w-full flex"> <Disclosure.Button className="flex w-full">
<ChevronRightIcon <ChevronRightIcon
className={`${open ? "transform rotate-90" : ""} w-5 h-5 text-neutral-500 ml-auto`} className={`${open ? "transform rotate-90" : ""} w-5 h-5 text-neutral-500 ml-auto`}
/> />
<span className="text-neutral-700 text-sm font-medium">Show advanced settings</span> <span className="text-sm font-medium text-neutral-700">Show advanced settings</span>
</Disclosure.Button> </Disclosure.Button>
<Disclosure.Panel className="space-y-4"> <Disclosure.Panel className="space-y-4">
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label <label
htmlFor="eventName" htmlFor="eventName"
className="text-sm flex font-medium text-neutral-700 mt-2"> className="flex mt-2 text-sm font-medium text-neutral-700">
Event name Event name
</label> </label>
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="mt-1 relative rounded-sm shadow-sm"> <div className="relative mt-1 rounded-sm shadow-sm">
<input <input
ref={eventNameRef} ref={eventNameRef}
type="text" type="text"
name="title" name="title"
id="title" id="title"
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm" className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Meeting with {USER}" placeholder="Meeting with {USER}"
defaultValue={eventType.eventName} defaultValue={eventType.eventName}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label <label
htmlFor="additionalFields" htmlFor="additionalFields"
className="text-sm flex font-medium text-neutral-700 mt-2"> className="flex mt-2 text-sm font-medium text-neutral-700">
Additional inputs Additional inputs
</label> </label>
</div> </div>
<div className="w-full"> <div className="w-full">
<ul className="w-96 mt-1"> <ul className="mt-1 w-96">
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( {customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
<li key={idx} className="bg-secondary-50 mb-2 p-2 border"> <li key={idx} className="p-2 mb-2 border bg-secondary-50">
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
<div> <div>
@ -663,7 +662,7 @@ export default function EventTypePage({
Edit Edit
</button> </button>
<button type="button" onClick={() => removeCustom(idx)}> <button type="button" onClick={() => removeCustom(idx)}>
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " /> <XIcon className="w-6 h-6 pl-1 border-l-2 hover:text-red-500 " />
</button> </button>
</div> </div>
</div> </div>
@ -672,10 +671,10 @@ export default function EventTypePage({
<li> <li>
<button <button
type="button" type="button"
className="bg-neutral-100 rounded-sm py-2 px-3 flex" className="flex px-3 py-2 rounded-sm bg-neutral-100"
onClick={() => setShowAddCustomModal(true)}> onClick={() => setShowAddCustomModal(true)}>
<PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" /> <PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" />
<span className="ml-1 text-neutral-700 text-sm font-medium"> <span className="ml-1 text-sm font-medium text-neutral-700">
Add an input Add an input
</span> </span>
</button> </button>
@ -683,11 +682,11 @@ export default function EventTypePage({
</ul> </ul>
</div> </div>
</div> </div>
<div className="block sm:flex items-center"> <div className="items-center block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label <label
htmlFor="requiresConfirmation" htmlFor="requiresConfirmation"
className="text-sm flex font-medium text-neutral-700"> className="flex text-sm font-medium text-neutral-700">
Opt-in booking Opt-in booking
</label> </label>
</div> </div>
@ -699,7 +698,7 @@ export default function EventTypePage({
id="requiresConfirmation" id="requiresConfirmation"
name="requiresConfirmation" name="requiresConfirmation"
type="checkbox" type="checkbox"
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded" className="w-4 h-4 border-gray-300 rounded focus:ring-primary-500 text-primary-600"
defaultChecked={eventType.requiresConfirmation} defaultChecked={eventType.requiresConfirmation}
/> />
</div> </div>
@ -716,10 +715,10 @@ export default function EventTypePage({
<hr className="border-neutral-200" /> <hr className="border-neutral-200" />
<div className="block sm:flex"> <div className="block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label <label
htmlFor="inviteesCanSchedule" htmlFor="inviteesCanSchedule"
className="text-sm flex font-medium text-neutral-700 mt-2"> className="flex mt-2 text-sm font-medium text-neutral-700">
Invitees can schedule Invitees can schedule
</label> </label>
</div> </div>
@ -750,7 +749,7 @@ export default function EventTypePage({
aria-hidden="true"> aria-hidden="true">
<span className="rounded-full bg-white w-1.5 h-1.5" /> <span className="rounded-full bg-white w-1.5 h-1.5" />
</div> </div>
<div className="lg:ml-3 flex flex-col"> <div className="flex flex-col lg:ml-3">
<RadioGroup.Label <RadioGroup.Label
as="span" as="span"
className={classnames( className={classnames(
@ -765,7 +764,7 @@ export default function EventTypePage({
type="text" type="text"
name="periodDays" name="periodDays"
id="" id=""
className="mr-2 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-12 sm:text-sm border-gray-300 rounded-sm" className="block w-12 mr-2 border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="30" placeholder="30"
defaultValue={eventType.periodDays || 30} defaultValue={eventType.periodDays || 30}
/> />
@ -773,7 +772,7 @@ export default function EventTypePage({
ref={periodDaysTypeRef} ref={periodDaysTypeRef}
id="" id=""
name="periodDaysType" name="periodDaysType"
className=" block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-sm" className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
defaultValue={ defaultValue={
eventType.periodCountCalendarDays ? "1" : "0" eventType.periodCountCalendarDays ? "1" : "0"
}> }>
@ -818,10 +817,10 @@ export default function EventTypePage({
<hr className="border-neutral-200" /> <hr className="border-neutral-200" />
<div className="block sm:flex"> <div className="block sm:flex">
<div className="min-w-44 mb-4 sm:mb-0"> <div className="mb-4 min-w-44 sm:mb-0">
<label <label
htmlFor="availability" htmlFor="availability"
className="text-sm flex font-medium text-neutral-700 mt-2"> className="flex mt-2 text-sm font-medium text-neutral-700">
Availability Availability
</label> </label>
</div> </div>
@ -838,15 +837,15 @@ export default function EventTypePage({
</> </>
)} )}
</Disclosure> </Disclosure>
<div className="mt-4 flex justify-end"> <div className="flex justify-end mt-4">
<Link href="/event-types"> <Link href="/event-types">
<a className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-neutral-700 bg-white hover:bg-neutral-100 border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black mr-2"> <a className="inline-flex items-center px-4 py-2 mr-2 text-sm font-medium bg-white border border-transparent rounded-sm shadow-sm text-neutral-700 hover:bg-neutral-100 border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Cancel Cancel
</a> </a>
</Link> </Link>
<button <button
type="submit" type="submit"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"> className="inline-flex items-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Update Update
</button> </button>
</div> </div>
@ -859,7 +858,7 @@ export default function EventTypePage({
/> />
</div> </div>
</div> </div>
<div className="w-full sm:w-2/12 ml-2 px-4 mt-8 sm:mt-0 min-w-32"> <div className="w-full px-4 mt-8 ml-2 sm:w-2/12 sm:mt-0 min-w-32">
<div className="space-y-4"> <div className="space-y-4">
<Switch <Switch
name="isHidden" name="isHidden"
@ -871,7 +870,7 @@ export default function EventTypePage({
href={"/" + user.username + "/" + eventType.slug} href={"/" + user.username + "/" + eventType.slug}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex text-md font-medium text-neutral-700"> className="flex font-medium text-md text-neutral-700">
<ExternalLinkIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" aria-hidden="true" /> <ExternalLinkIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" aria-hidden="true" />
Preview Preview
</a> </a>
@ -883,12 +882,12 @@ export default function EventTypePage({
showToast("Link copied!", "success"); showToast("Link copied!", "success");
}} }}
type="button" type="button"
className="flex text-md font-medium text-neutral-700"> className="flex font-medium text-md text-neutral-700">
<LinkIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" /> <LinkIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" />
Copy link Copy link
</button> </button>
<Dialog> <Dialog>
<DialogTrigger className="flex text-md font-medium text-neutral-700"> <DialogTrigger className="flex font-medium text-md text-neutral-700">
<TrashIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" /> <TrashIcon className="w-4 h-4 mt-1 mr-2 text-neutral-500" />
Delete Delete
</DialogTrigger> </DialogTrigger>
@ -906,26 +905,26 @@ export default function EventTypePage({
</div> </div>
{showLocationModal && ( {showLocationModal && (
<div <div
className="fixed z-50 inset-0 overflow-y-auto" className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title" aria-labelledby="modal-title"
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div <div
className="fixed inset-0 bg-gray-500 z-0 bg-opacity-75 transition-opacity" className="fixed inset-0 z-0 transition-opacity bg-gray-500 bg-opacity-75"
aria-hidden="true"></div> aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> <div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-sm shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4"> <div className="mb-4 sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10"> <div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10">
<LocationMarkerIcon className="h-6 w-6 text-primary-600" /> <LocationMarkerIcon className="w-6 h-6 text-primary-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> <h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
Edit location Edit location
</h3> </h3>
</div> </div>
@ -937,7 +936,7 @@ export default function EventTypePage({
options={locationOptions} options={locationOptions}
isSearchable="false" isSearchable="false"
classNamePrefix="react-select" classNamePrefix="react-select"
className="react-select-container rounded-sm border border-gray-300 flex-1 block w-full focus:ring-primary-500 focus:border-primary-500 min-w-0 sm:text-sm my-4" className="flex-1 block w-full min-w-0 my-4 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
onChange={setSelectedLocation} onChange={setSelectedLocation}
/> />
<LocationOptions /> <LocationOptions />
@ -945,7 +944,7 @@ export default function EventTypePage({
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">
Update Update
</button> </button>
<button onClick={closeLocationModal} type="button" className="btn btn-white mr-2"> <button onClick={closeLocationModal} type="button" className="mr-2 btn btn-white">
Cancel Cancel
</button> </button>
</div> </div>
@ -956,13 +955,13 @@ export default function EventTypePage({
)} )}
{showAddCustomModal && ( {showAddCustomModal && (
<div <div
className="fixed z-50 inset-0 overflow-y-auto" className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title" aria-labelledby="modal-title"
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div <div
className="fixed inset-0 bg-gray-500 z-0 bg-opacity-75 transition-opacity" className="fixed inset-0 z-0 transition-opacity bg-gray-500 bg-opacity-75"
aria-hidden="true" aria-hidden="true"
/> />
@ -970,13 +969,13 @@ export default function EventTypePage({
&#8203; &#8203;
</span> </span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> <div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-sm shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4"> <div className="mb-4 sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10"> <div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-primary-600" /> <PlusIcon className="w-6 h-6 text-primary-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> <h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
Add new custom input field Add new custom input field
</h3> </h3>
<div> <div>
@ -997,7 +996,7 @@ export default function EventTypePage({
options={inputOptions} options={inputOptions}
isSearchable="false" isSearchable="false"
required required
className="mb-2 flex-1 block w-full focus:ring-primary-500 focus:border-primary-500 min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300 mt-1" className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
onChange={setSelectedInputOption} onChange={setSelectedInputOption}
/> />
</div> </div>
@ -1011,7 +1010,7 @@ export default function EventTypePage({
name="label" name="label"
id="label" id="label"
required required
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm" className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
defaultValue={selectedCustomInput?.label} defaultValue={selectedCustomInput?.label}
/> />
</div> </div>
@ -1021,7 +1020,7 @@ export default function EventTypePage({
id="required" id="required"
name="required" name="required"
type="checkbox" type="checkbox"
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded mr-2" className="w-4 h-4 mr-2 border-gray-300 rounded focus:ring-primary-500 text-primary-600"
defaultChecked={selectedCustomInput?.required ?? true} defaultChecked={selectedCustomInput?.required ?? true}
/> />
<label htmlFor="required" className="block text-sm font-medium text-gray-700"> <label htmlFor="required" className="block text-sm font-medium text-gray-700">
@ -1033,7 +1032,7 @@ export default function EventTypePage({
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">
Save Save
</button> </button>
<button onClick={closeAddCustomModal} type="button" className="btn btn-white mr-2"> <button onClick={closeAddCustomModal} type="button" className="mr-2 btn btn-white">
Cancel Cancel
</button> </button>
</div> </div>

View file

@ -1,14 +1,15 @@
import prisma from '../../lib/prisma'; import { GetServerSidePropsContext } from "next";
import prisma from "../../lib/prisma";
export default function Type(props) { export default function Type() {
// Just redirect to the schedule page to reschedule it. // Just redirect to the schedule page to reschedule it.
return null; return null;
} }
export async function getServerSideProps(context) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const booking = await prisma.booking.findFirst({ const booking = await prisma.booking.findFirst({
where: { where: {
uid: context.query.uid, uid: context.query.uid as string,
}, },
select: { select: {
id: true, id: true,
@ -18,14 +19,20 @@ export async function getServerSideProps(context) {
description: true, description: true,
startTime: true, startTime: true,
endTime: true, endTime: true,
attendees: true attendees: true,
} },
}); });
if (!booking?.user || !booking.eventType) {
return {
notFound: true,
} as const;
}
return { return {
redirect: { redirect: {
destination: '/' + booking.user.username + '/' + booking.eventType.slug + '?rescheduleUid=' + context.query.uid, destination:
"/" + booking.user.username + "/" + booking.eventType.slug + "?rescheduleUid=" + context.query.uid,
permanent: false, permanent: false,
}, },
} };
} }

View file

@ -28,7 +28,7 @@ export default function Settings(props) {
const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme }); const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme });
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart }); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart });
const [imageSrc, setImageSrc] = useState<string>(''); const [imageSrc, setImageSrc] = useState<string>("");
const [hasErrors, setHasErrors] = useState(false); const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
@ -46,13 +46,16 @@ export default function Settings(props) {
const handleAvatarChange = (newAvatar) => { const handleAvatarChange = (newAvatar) => {
avatarRef.current.value = newAvatar; avatarRef.current.value = newAvatar;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
).set;
nativeInputValueSetter.call(avatarRef.current, newAvatar); nativeInputValueSetter.call(avatarRef.current, newAvatar);
const ev2 = new Event('input', { bubbles: true}); const ev2 = new Event("input", { bubbles: true });
avatarRef.current.dispatchEvent(ev2); avatarRef.current.dispatchEvent(ev2);
updateProfileHandler(ev2); updateProfileHandler(ev2);
setImageSrc(newAvatar); setImageSrc(newAvatar);
} };
const handleError = async (resp) => { const handleError = async (resp) => {
if (!resp.ok) { if (!resp.ok) {

View file

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View file

@ -7,14 +7,11 @@ it("can decorate using whereAndSelect", async () => {
expect(queryObj).toStrictEqual({ where: { id: 1 }, select: { example: true } }); expect(queryObj).toStrictEqual({ where: { id: 1 }, select: { example: true } });
}, },
{ id: 1 }, { id: 1 },
[ ["example"]
"example",
]
); );
}); });
it("can do nested selects using . seperator", async () => { it("can do nested selects using . seperator", async () => {
whereAndSelect( whereAndSelect(
(queryObj) => { (queryObj) => {
expect(queryObj).toStrictEqual({ expect(queryObj).toStrictEqual({
@ -33,11 +30,7 @@ it("can do nested selects using . seperator", async () => {
}); });
}, },
{ uid: 1 }, { uid: 1 },
[ ["description", "attendees.email", "attendees.name"]
"description",
"attendees.email",
"attendees.name",
]
); );
}); });
@ -55,7 +48,7 @@ it("can handle nesting deeply", async () => {
email: { email: {
select: { select: {
nested: true, nested: true,
} },
}, },
name: true, name: true,
}, },
@ -64,11 +57,7 @@ it("can handle nesting deeply", async () => {
}); });
}, },
{ uid: 1 }, { uid: 1 },
[ ["description", "attendees.email.nested", "attendees.name"]
"description",
"attendees.email.nested",
"attendees.name",
]
); );
}); });
@ -91,18 +80,12 @@ it("can handle nesting multiple", async () => {
select: { select: {
id: true, id: true,
name: true, name: true,
} },
} },
} },
}); });
}, },
{ uid: 1 }, { uid: 1 },
[ ["description", "attendees.email", "attendees.name", "bookings.id", "bookings.name"]
"description",
"attendees.email",
"attendees.name",
"bookings.id",
"bookings.name",
]
); );
}); });

View file

@ -8,50 +8,50 @@ import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
MockDate.set('2021-06-20T11:59:59Z'); MockDate.set("2021-06-20T11:59:59Z");
it('can fit 24 hourly slots for an empty day', async () => { it("can fit 24 hourly slots for an empty day", async () => {
// 24h in a day. // 24h in a day.
expect(getSlots({ expect(
inviteeDate: dayjs().add(1, 'day'), getSlots({
inviteeDate: dayjs().add(1, "day"),
frequency: 60, frequency: 60,
workingHours: [ workingHours: [{ days: [...Array(7).keys()], startTime: 0, endTime: 1440 }],
{ days: [...Array(7).keys()], startTime: 0, endTime: 1440 } organizerTimeZone: "Europe/London",
], })
organizerTimeZone: 'Europe/London' ).toHaveLength(24);
})).toHaveLength(24);
}); });
it('only shows future booking slots on the same day', async () => { it("only shows future booking slots on the same day", async () => {
// The mock date is 1s to midday, so 12 slots should be open given 0 booking notice. // The mock date is 1s to midday, so 12 slots should be open given 0 booking notice.
expect(getSlots({ expect(
getSlots({
inviteeDate: dayjs(), inviteeDate: dayjs(),
frequency: 60, frequency: 60,
workingHours: [ workingHours: [{ days: [...Array(7).keys()], startTime: 0, endTime: 1440 }],
{ days: [...Array(7).keys()], startTime: 0, endTime: 1440 } organizerTimeZone: "GMT",
], })
organizerTimeZone: 'GMT' ).toHaveLength(12);
})).toHaveLength(12);
}); });
it('can cut off dates that due to invitee timezone differences fall on the next day', async () => { it("can cut off dates that due to invitee timezone differences fall on the next day", async () => {
expect(getSlots({ expect(
inviteeDate: dayjs().tz('Europe/Amsterdam').startOf('day'), // time translation +01:00 getSlots({
inviteeDate: dayjs().tz("Europe/Amsterdam").startOf("day"), // time translation +01:00
frequency: 60, frequency: 60,
workingHours: [ workingHours: [{ days: [0], startTime: 1380, endTime: 1440 }],
{ days: [0], startTime: 1380, endTime: 1440 } organizerTimeZone: "Europe/London",
], })
organizerTimeZone: 'Europe/London' ).toHaveLength(0);
})).toHaveLength(0);
}); });
it('can cut off dates that due to invitee timezone differences fall on the previous day', async () => { it("can cut off dates that due to invitee timezone differences fall on the previous day", async () => {
expect(getSlots({ expect(
inviteeDate: dayjs().startOf('day'), // time translation -01:00 getSlots({
inviteeDate: dayjs().startOf("day"), // time translation -01:00
frequency: 60, frequency: 60,
workingHours: [ workingHours: [{ days: [0], startTime: 0, endTime: 60 }],
{ days: [0], startTime: 0, endTime: 60 } organizerTimeZone: "Europe/London",
], })
organizerTimeZone: 'Europe/London' ).toHaveLength(0);
})).toHaveLength(0);
}); });

View file

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@components/*": ["components/*"], "@components/*": ["components/*"],
@ -23,12 +19,6 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve"
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "lib/*.js"],
"next-env.d.ts", "exclude": ["node_modules"]
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
} }