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:
parent
5a9961f608
commit
f63aa5d550
36 changed files with 988 additions and 959 deletions
21
.github/workflows/lint.yml
vendored
Normal file
21
.github/workflows/lint.yml
vendored
Normal 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
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Cropper from "react-easy-crop";
|
||||||
import { useState, useCallback, useRef } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import Slider from "./Slider";
|
import Slider from "./Slider";
|
||||||
|
|
||||||
export default function ImageUploader({target, id, buttonMsg, handleAvatarChange, imageRef}){
|
export default function ImageUploader({ target, id, buttonMsg, handleAvatarChange, imageRef }) {
|
||||||
const imageFileRef = useRef<HTMLInputElement>();
|
const imageFileRef = useRef<HTMLInputElement>();
|
||||||
const [imageDataUrl, setImageDataUrl] = useState<string>();
|
const [imageDataUrl, setImageDataUrl] = useState<string>();
|
||||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState();
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState();
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
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
|
||||||
className="slider mt-2"
|
className="slider mt-2"
|
||||||
min={min}
|
min={min}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
useEffect( () => {
|
|
||||||
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>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
43
lib/clock.ts
43
lib/clock.ts
|
@ -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 };
|
||||||
|
|
|
@ -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,8 +31,9 @@ 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) => {
|
||||||
let url: string = process.env.BASE_URL + "/settings/teams";
|
let url: string = process.env.BASE_URL + "/settings/teams";
|
||||||
|
@ -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, "");
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
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";
|
||||||
case "office365_calendar":
|
case "office365_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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import React, {useContext} from 'react'
|
import React, { useContext } from "react";
|
||||||
import {jitsuClient, JitsuClient} from "@jitsu/sdk-js";
|
import { jitsuClient, JitsuClient } from "@jitsu/sdk-js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumeration of all event types that are being sent
|
* Enumeration of all event types that are being sent
|
||||||
* 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 };
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { getSession } from "@lib/auth";
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma from "../../../lib/prisma";
|
||||||
|
|
||||||
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) {
|
||||||
res.status(401).json({message: "Not authenticated"});
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
|
@ -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" });
|
||||||
}
|
}
|
|
@ -4,10 +4,10 @@ import prisma from "../../../lib/prisma";
|
||||||
import { IntegrationCalendar, listCalendars } from "../../../lib/calendarClient";
|
import { IntegrationCalendar, listCalendars } from "../../../lib/calendarClient";
|
||||||
|
|
||||||
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) {
|
||||||
res.status(401).json({message: "Not authenticated"});
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,26 +43,28 @@ 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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { getSession } from "@lib/auth";
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma from "../../../lib/prisma";
|
||||||
|
|
||||||
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) {
|
||||||
res.status(401).json({message: "Not authenticated"});
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,26 +17,29 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,42 +1,37 @@
|
||||||
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
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;
|
||||||
|
|
||||||
// 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 {client_secret, client_id, redirect_uris} = JSON.parse(credentials).web;
|
return;
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
|
||||||
|
|
||||||
// Convert to token
|
|
||||||
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: {
|
|
||||||
type: 'google_calendar',
|
|
||||||
key: token,
|
|
||||||
userId: session.user.id
|
|
||||||
}
|
}
|
||||||
|
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 oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
||||||
|
const token = await oAuth2Client.getToken(code);
|
||||||
|
|
||||||
|
await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
key: token as any,
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect('/integrations');
|
res.redirect("/integrations");
|
||||||
resolve();
|
|
||||||
}));
|
|
||||||
}
|
}
|
|
@ -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() {
|
res.status(200).json({ url: 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() });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,38 +7,57 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,25 +5,32 @@ 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
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";
|
||||||
|
|
||||||
const client_id = process.env.ZOOM_CLIENT_ID;
|
const client_id = process.env.ZOOM_CLIENT_ID;
|
||||||
|
@ -9,31 +9,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
|
@ -1,22 +1,21 @@
|
||||||
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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,38 +1,33 @@
|
||||||
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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
return res.status(404).json({message: "Invalid team"});
|
return res.status(404).json({ message: "Invalid team" });
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
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) {
|
||||||
res.status(401).json({message: "Not authenticated"});
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
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" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,33 @@
|
||||||
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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
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);
|
||||||
|
|
|
@ -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">
|
||||||
​
|
​
|
||||||
</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({
|
||||||
​
|
​
|
||||||
</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>
|
||||||
|
|
|
@ -1,31 +1,38 @@
|
||||||
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,
|
||||||
user: {select: {username: true}},
|
user: { select: { username: true } },
|
||||||
eventType: {select: {slug: true}},
|
eventType: { select: { slug: true } },
|
||||||
title: true,
|
title: true,
|
||||||
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,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -3,4 +3,4 @@ module.exports = {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -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",
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue