Fixed linting errors

This commit is contained in:
nicolas 2021-07-20 20:18:26 +02:00
commit e0abbbb2f1
7 changed files with 519 additions and 442 deletions

View File

@ -1,9 +1,8 @@
import { GiftIcon } from "@heroicons/react/outline";
export default function DonateBanner() {
if (location.hostname.endsWith(".calendso.com")) {
return null;
}
if (location.hostname.endsWith(".calendso.com")) {
return null;
}
return (
<>
@ -17,21 +16,19 @@ return null;
<GiftIcon className="h-6 w-6 text-white" aria-hidden="true" />
</span>
<p className="ml-3 font-medium text-white truncate">
<span className="md:hidden">
Support the ongoing development
</span>
<span className="md:hidden">Support the ongoing development</span>
<span className="hidden md:inline">
You're using the free self-hosted version. Support the
ongoing development by making a donation.
You&apos;re using the free self-hosted version. Support the ongoing development by making
a donation.
</span>
</p>
</div>
<div className="order-3 mt-2 flex-shrink-0 w-full sm:order-2 sm:mt-0 sm:w-auto">
<a
target="_blank"
rel="noreferrer"
href="https://calendso.com/donate"
className="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 bg-white hover:bg-blue-50"
>
className="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 bg-white hover:bg-blue-50">
Donate
</a>
</div>

View File

@ -2,9 +2,6 @@
@apply font-medium;
} */
.text {
}
.text--body {
@apply text-lg leading-relaxed;
}

View File

@ -1,31 +1,40 @@
const withTM = require('next-transpile-modules')(['react-timezone-select']);
import nextTranspileModules from "next-transpile-modules";
const withTM = nextTranspileModules(["react-timezone-select"]);
// TODO: Revisit this later with getStaticProps in App
if (process.env.NEXTAUTH_URL) {
process.env.BASE_URL = process.env.NEXTAUTH_URL.replace('/api/auth', '');
process.env.BASE_URL = process.env.NEXTAUTH_URL.replace("/api/auth", "");
}
if ( ! process.env.EMAIL_FROM ) {
console.warn('\x1b[33mwarn', '\x1b[0m', 'EMAIL_FROM environment variable is not set, this may indicate mailing is currently disabled. Please refer to the .env.example file.');
if (!process.env.EMAIL_FROM) {
console.warn(
"\x1b[33mwarn",
"\x1b[0m",
"EMAIL_FROM environment variable is not set, this may indicate mailing is currently disabled. Please refer to the .env.example file."
);
}
if (process.env.BASE_URL) {
process.env.NEXTAUTH_URL = process.env.BASE_URL + '/api/auth';
process.env.NEXTAUTH_URL = process.env.BASE_URL + "/api/auth";
}
const validJson = (jsonString) => {
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
catch (e) {}
return false;
}
} catch (e) {
console.error(e);
}
return false;
};
if (process.env.GOOGLE_API_CREDENTIALS && ! validJson(process.env.GOOGLE_API_CREDENTIALS)) {
console.warn('\x1b[33mwarn', '\x1b[0m', "- Disabled 'Google Calendar' integration. Reason: Invalid value for GOOGLE_API_CREDENTIALS environment variable. When set, this value needs to contain valid JSON like {\"web\":{\"client_id\":\"<clid>\",\"client_secret\":\"<secret>\",\"redirect_uris\":[\"<yourhost>/api/integrations/googlecalendar/callback>\"]}. You can download this JSON from your OAuth Client @ https://console.cloud.google.com/apis/credentials.");
if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CREDENTIALS)) {
console.warn(
"\x1b[33mwarn",
"\x1b[0m",
'- Disabled \'Google Calendar\' integration. Reason: Invalid value for GOOGLE_API_CREDENTIALS environment variable. When set, this value needs to contain valid JSON like {"web":{"client_id":"<clid>","client_secret":"<secret>","redirect_uris":["<yourhost>/api/integrations/googlecalendar/callback>"]}. You can download this JSON from your OAuth Client @ https://console.cloud.google.com/apis/credentials.'
);
}
module.exports = withTM({
@ -38,10 +47,10 @@ module.exports = withTM({
async redirects() {
return [
{
source: '/settings',
destination: '/settings/profile',
source: "/settings",
destination: "/settings/profile",
permanent: true,
}
]
}
},
];
},
});

View File

@ -12,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (req.method == "PATCH" || req.method == "POST") {
const data = {
title: req.body.title,
slug: req.body.slug,
slug: req.body.slug.trim(),
description: req.body.description,
length: parseInt(req.body.length),
hidden: req.body.hidden,

View File

@ -1,41 +1,45 @@
import Head from 'next/head';
import Link from 'next/link';
import { CheckIcon } from '@heroicons/react/outline';
import Head from "next/head";
import Link from "next/link";
import { CheckIcon } from "@heroicons/react/outline";
export default function Logout() {
return (
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<Head>
<title>Logged out - Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<div>
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<CheckIcon className="h-6 w-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
You've been logged out
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
We hope to see you again soon!
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login">
<a className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm">
Go back to the login page
</a>
</Link>
</div>
</div>
return (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<Head>
<title>Logged out - Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<div>
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<CheckIcon className="h-6 w-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
You&apos;ve been logged out
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">We hope to see you again soon!</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login">
<a className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm">
Go back to the login page
</a>
</Link>
</div>
</div>
);
}
</div>
</div>
);
}

View File

@ -155,7 +155,7 @@ export default function Availability(props) {
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{props.types.map((eventType) => (
<tr key={eventType}>
<tr key={eventType.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 align-top">
{eventType.title}
{eventType.hidden && (
@ -341,7 +341,7 @@ export default function Availability(props) {
Hide this event type
</label>
<p className="text-gray-500">
Hide the event type from your page, so it can only be booked through its URL.
Hide the event type from your page, so it can only be booked through it&apos;s URL.
</p>
</div>
</div>

View File

@ -1,387 +1,457 @@
import Head from 'next/head';
import Link from 'next/link';
import prisma from '../../lib/prisma';
import Shell from '../../components/Shell';
import {useEffect, useState} from 'react';
import {getSession, useSession} from 'next-auth/client';
import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid';
import {InformationCircleIcon} from '@heroicons/react/outline';
import {Switch} from '@headlessui/react'
import Head from "next/head";
import Link from "next/link";
import prisma from "../../lib/prisma";
import Shell from "../../components/Shell";
import { useEffect, useState } from "react";
import { getSession, useSession } from "next-auth/client";
import {
CalendarIcon,
CheckCircleIcon,
ChevronRightIcon,
PlusIcon,
XCircleIcon,
} from "@heroicons/react/solid";
import { InformationCircleIcon } from "@heroicons/react/outline";
import { Switch } from "@headlessui/react";
export default function Home({ integrations }) {
const [session, loading] = useSession();
const [showAddModal, setShowAddModal] = useState(false);
const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false);
const [selectableCalendars, setSelectableCalendars] = useState([]);
const [, loading] = useSession();
const [showAddModal, setShowAddModal] = useState(false);
const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false);
const [selectableCalendars, setSelectableCalendars] = useState([]);
function toggleAddModal() {
setShowAddModal(!showAddModal);
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
function toggleShowCalendarModal() {
setShowSelectCalendarModal(!showSelectCalendarModal);
}
function loadCalendars() {
fetch("api/availability/calendar")
.then((response) => response.json())
.then((data) => {
setSelectableCalendars(data);
});
}
function integrationHandler(type) {
fetch("/api/integrations/" + type.replace("_", "") + "/add")
.then((response) => response.json())
.then((data) => (window.location.href = data.url));
}
function calendarSelectionHandler(calendar) {
return (selected) => {
const cals = [...selectableCalendars];
const i = cals.findIndex((c) => c.externalId === calendar.externalId);
cals[i].selected = selected;
setSelectableCalendars(cals);
if (selected) {
fetch("api/availability/calendar", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(cals[i]),
}).then((response) => response.json());
} else {
fetch("api/availability/calendar", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(cals[i]),
}).then((response) => response.json());
}
};
}
function getCalendarIntegrationImage(integrationType: string) {
switch (integrationType) {
case "google_calendar":
return "integrations/google-calendar.png";
case "office365_calendar":
return "integrations/office-365.png";
default:
return "";
}
}
function toggleShowCalendarModal() {
setShowSelectCalendarModal(!showSelectCalendarModal);
}
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
function loadCalendars() {
fetch('api/availability/calendar')
.then((response) => response.json())
.then(data => {
setSelectableCalendars(data)
});
}
useEffect(loadCalendars, [integrations]);
function integrationHandler(type) {
fetch('/api/integrations/' + type.replace('_', '') + '/add')
.then((response) => response.json())
.then((data) => window.location.href = data.url);
}
if (loading) {
return <div className="loader"></div>;
}
function calendarSelectionHandler(calendar) {
return (selected) => {
let cals = [...selectableCalendars];
let i = cals.findIndex(c => c.externalId === calendar.externalId);
cals[i].selected = selected;
setSelectableCalendars(cals);
if (selected) {
fetch('api/availability/calendar', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cals[i])
}).then((response) => response.json());
} else {
fetch('api/availability/calendar', {
method: 'DELETE', headers: {
'Content-Type': 'application/json'
}, body: JSON.stringify(cals[i])
}).then((response) => response.json());
}
}
}
return (
<div>
<Head>
<title>Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
function getCalendarIntegrationImage(integrationType: string){
switch (integrationType) {
case "google_calendar": return "integrations/google-calendar.png";
case "office365_calendar": return "integrations/office-365.png";
default: return "";
}
}
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
useEffect(loadCalendars, [integrations]);
if (loading) {
return <div className="loader"></div>;
}
return (
<div>
<Head>
<title>Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Integrations" noPaddingBottom>
<div className="text-right py-2">
<button onClick={toggleAddModal} type="button"
className="px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Add new integration
</button>
</div>
<div className="bg-white shadow overflow-hidden rounded-lg mb-8">
{integrations.filter( (ig) => ig.credential ).length !== 0 ? <ul className="divide-y divide-gray-200">
{integrations.filter(ig => ig.credential).map( (ig) => (<li>
<Link href={"/integrations/" + ig.credential.id}>
<a className="block hover:bg-gray-50">
<div className="flex items-center px-4 py-4 sm:px-6">
<div className="min-w-0 flex-1 flex items-center">
<div className="flex-shrink-0">
<img className="h-10 w-10 mr-2" src={ig.imageSrc} alt={ig.title} />
</div>
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
<div>
<p className="text-sm font-medium text-blue-600 truncate">{ig.title}</p>
<p className="flex items-center text-sm text-gray-500">
{ig.type.endsWith('_calendar') && <span className="truncate">Calendar Integration</span>}
{ig.type.endsWith('_video') && <span className="truncate">Video Conferencing</span>}
</p>
</div>
<div className="hidden md:block">
{ig.credential.key && <p className="mt-2 flex items-center text text-gray-500">
<CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" />
Connected
</p>}
{!ig.credential.key && <p className="mt-3 flex items-center text text-gray-500">
<XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" />
Not connected
</p>}
</div>
</div>
<div>
<ChevronRightIcon className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
</a>
</Link>
</li>))}
</ul>
:
<div className="bg-white shadow rounded-lg">
<div className="flex">
<div className="py-9 pl-8">
<InformationCircleIcon className="text-blue-600 w-16" />
</div>
<div className="py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
You don't have any integrations added.
</h3>
<div className="mt-2 text-sm text-gray-500">
<p>
You currently do not have any integrations set up. Add your first integration to get started.
</p>
</div>
<div className="mt-3 text-sm">
<button onClick={toggleAddModal} className="font-medium text-blue-600 hover:text-blue-500"> Add your first integration <span aria-hidden="true">&rarr;</span></button>
</div>
</div>
</div>
</div>
}
</div>
{showAddModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" 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">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-blue-600" />
</div>
<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">
Add a new integration
</h3>
<div>
<p className="text-sm text-gray-400">
Link a new integration to your account.
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{integrations.filter( (integration) => integration.installed ).map( (integration) => (<li className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} />
</div>
<div className="w-10/12">
<h2 className="text-gray-800 font-medium">{ integration.title }</h2>
<p className="text-gray-400 text-sm">{ integration.description }</p>
</div>
<div className="w-2/12 text-right pt-2">
<button onClick={() => integrationHandler(integration.type)} className="font-medium text-blue-600 hover:text-blue-500">Add</button>
</div>
</li>))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button onClick={toggleAddModal} type="button" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Select calendars
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Select which calendars are checked for availability to prevent double bookings.
</p>
</div>
<div className="mt-5">
<button type="button" onClick={toggleShowCalendarModal} className="btn btn-primary">
Select calendars
</button>
</div>
</div>
</div>
{showSelectCalendarModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" 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">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<CalendarIcon className="h-6 w-6 text-blue-600" />
</div>
<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">
Select calendars
</h3>
<div>
<p className="text-sm text-gray-400">
If no entry is selected, all calendars will be checked
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{selectableCalendars.map( (calendar) => (<li className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img className="h-8 w-8 mr-2" src={getCalendarIntegrationImage(calendar.integration)} alt={calendar.integration} />
</div>
<div className="w-10/12 pt-3">
<h2 className="text-gray-800 font-medium">{ calendar.name }</h2>
</div>
<div className="w-2/12 text-right pt-3">
<Switch
checked={calendar.selected}
onChange={calendarSelectionHandler(calendar)}
className={classNames(
calendar.selected ? 'bg-indigo-600' : 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
)}
>
<span className="sr-only">Select calendar</span>
<span
aria-hidden="true"
className={classNames(
calendar.selected ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
</div>
</li>))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button onClick={toggleShowCalendarModal} type="button" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
}
</Shell>
<Shell heading="Integrations" noPaddingBottom>
<div className="text-right py-2">
<button
onClick={toggleAddModal}
type="button"
className="px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Add new integration
</button>
</div>
);
<div className="bg-white shadow overflow-hidden rounded-lg mb-8">
{integrations.filter((ig) => ig.credential).length !== 0 ? (
<ul className="divide-y divide-gray-200">
{integrations
.filter((ig) => ig.credential)
.map((ig) => (
<li key={ig.id}>
<Link href={"/integrations/" + ig.credential.id}>
<a className="block hover:bg-gray-50">
<div className="flex items-center px-4 py-4 sm:px-6">
<div className="min-w-0 flex-1 flex items-center">
<div className="flex-shrink-0">
<img className="h-10 w-10 mr-2" src={ig.imageSrc} alt={ig.title} />
</div>
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
<div>
<p className="text-sm font-medium text-blue-600 truncate">{ig.title}</p>
<p className="flex items-center text-sm text-gray-500">
{ig.type.endsWith("_calendar") && (
<span className="truncate">Calendar Integration</span>
)}
{ig.type.endsWith("_video") && (
<span className="truncate">Video Conferencing</span>
)}
</p>
</div>
<div className="hidden md:block">
{ig.credential.key && (
<p className="mt-2 flex items-center text text-gray-500">
<CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" />
Connected
</p>
)}
{!ig.credential.key && (
<p className="mt-3 flex items-center text text-gray-500">
<XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" />
Not connected
</p>
)}
</div>
</div>
<div>
<ChevronRightIcon className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
</a>
</Link>
</li>
))}
</ul>
) : (
<div className="bg-white shadow rounded-lg">
<div className="flex">
<div className="py-9 pl-8">
<InformationCircleIcon className="text-blue-600 w-16" />
</div>
<div className="py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
You don&apos;t have any integrations added.
</h3>
<div className="mt-2 text-sm text-gray-500">
<p>
You currently do not have any integrations set up. Add your first integration to get
started.
</p>
</div>
<div className="mt-3 text-sm">
<button
onClick={toggleAddModal}
className="font-medium text-blue-600 hover:text-blue-500">
{" "}
Add your first integration <span aria-hidden="true">&rarr;</span>
</button>
</div>
</div>
</div>
</div>
)}
</div>
{showAddModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
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">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-blue-600" />
</div>
<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">
Add a new integration
</h3>
<div>
<p className="text-sm text-gray-400">Link a new integration to your account.</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{integrations
.filter((integration) => integration.installed)
.map((integration) => (
<li key={integration.type} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img
className="h-8 w-8 mr-2"
src={integration.imageSrc}
alt={integration.title}
/>
</div>
<div className="w-10/12">
<h2 className="text-gray-800 font-medium">{integration.title}</h2>
<p className="text-gray-400 text-sm">{integration.description}</p>
</div>
<div className="w-2/12 text-right pt-2">
<button
onClick={() => integrationHandler(integration.type)}
className="font-medium text-blue-600 hover:text-blue-500">
Add
</button>
</div>
</li>
))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
onClick={toggleAddModal}
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
)}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Select calendars</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>Select which calendars are checked for availability to prevent double bookings.</p>
</div>
<div className="mt-5">
<button type="button" onClick={toggleShowCalendarModal} className="btn btn-primary">
Select calendars
</button>
</div>
</div>
</div>
{showSelectCalendarModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
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">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<CalendarIcon className="h-6 w-6 text-blue-600" />
</div>
<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">
Select calendars
</h3>
<div>
<p className="text-sm text-gray-400">
If no entry is selected, all calendars will be checked
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{selectableCalendars.map((calendar) => (
<li key={calendar.name} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img
className="h-8 w-8 mr-2"
src={getCalendarIntegrationImage(calendar.integration)}
alt={calendar.integration}
/>
</div>
<div className="w-10/12 pt-3">
<h2 className="text-gray-800 font-medium">{calendar.name}</h2>
</div>
<div className="w-2/12 text-right pt-3">
<Switch
checked={calendar.selected}
onChange={calendarSelectionHandler(calendar)}
className={classNames(
calendar.selected ? "bg-indigo-600" : "bg-gray-200",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
)}>
<span className="sr-only">Select calendar</span>
<span
aria-hidden="true"
className={classNames(
calendar.selected ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
</div>
</li>
))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
onClick={toggleShowCalendarModal}
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
)}
</Shell>
</div>
);
}
const validJson = (jsonString: string) => {
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
catch (e) {}
return false;
}
} catch (e) {
console.error(e);
}
return false;
};
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true
}
});
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
},
});
const credentials = await prisma.credential.findMany({
where: {
userId: user.id,
},
select: {
id: true,
type: true,
key: true
}
});
const credentials = await prisma.credential.findMany({
where: {
userId: user.id,
},
select: {
id: true,
type: true,
key: true,
},
});
const integrations = [ {
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
credential: credentials.find( (integration) => integration.type === "google_calendar" ) || null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.png",
description: "For personal and business calendars",
}, {
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
credential: credentials.find( (integration) => integration.type === "office365_calendar" ) || null,
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/office-365.png",
description: "For personal and business calendars",
}, {
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
type: "zoom_video",
credential: credentials.find( (integration) => integration.type === "zoom_video" ) || null,
title: "Zoom",
imageSrc: "integrations/zoom.png",
description: "Video Conferencing",
} ];
const integrations = [
{
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
credential: credentials.find((integration) => integration.type === "google_calendar") || null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.png",
description: "For personal and business calendars",
},
{
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
credential: credentials.find((integration) => integration.type === "office365_calendar") || null,
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/office-365.png",
description: "For personal and business calendars",
},
{
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
type: "zoom_video",
credential: credentials.find((integration) => integration.type === "zoom_video") || null,
title: "Zoom",
imageSrc: "integrations/zoom.png",
description: "Video Conferencing",
},
];
return {
props: {integrations},
}
return {
props: { integrations },
};
}