commit
dcc762d3e4
14 changed files with 922 additions and 680 deletions
|
@ -1,7 +1,10 @@
|
|||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>'
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
|
||||
# Remove this var if you don't want Calendso to collect anonymous usage
|
||||
NEXT_PUBLIC_TELEMETRY_KEY=js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r
|
||||
|
||||
# Used for the Office 365 / Outlook.com Calendar integration
|
||||
MS_GRAPH_CLIENT_ID=
|
||||
MS_GRAPH_CLIENT_SECRET=
|
|
@ -1,14 +1,22 @@
|
|||
import Link from 'next/link';
|
||||
import { useState } from "react";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { signOut, useSession } from 'next-auth/client';
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||
import {TelemetryContext, useTelemetry} from "../lib/telemetry";
|
||||
|
||||
export default function Shell(props) {
|
||||
const router = useRouter();
|
||||
const [ session, loading ] = useSession();
|
||||
const [ profileDropdownExpanded, setProfileDropdownExpanded ] = useState(false);
|
||||
const [ mobileMenuExpanded, setMobileMenuExpanded ] = useState(false);
|
||||
let telemetry = useTelemetry();
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu((jitsu) => {
|
||||
return jitsu.track('page_view', {page_url: router.pathname, page_title: "", source_ip: ""})
|
||||
});
|
||||
}, [telemetry])
|
||||
|
||||
const toggleProfileDropdown = () => {
|
||||
setProfileDropdownExpanded(!profileDropdownExpanded);
|
||||
|
|
62
lib/telemetry.ts
Normal file
62
lib/telemetry.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, {useContext} from 'react'
|
||||
import {jitsuClient, JitsuClient} from "@jitsu/sdk-js";
|
||||
|
||||
|
||||
/**
|
||||
* Telemetry client
|
||||
*/
|
||||
export type TelemetryClient = {
|
||||
/**
|
||||
* Use it as: withJitsu((jitsu) => {return jitsu.track()}). If telemetry is disabled, the callback will ignored
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
withJitsu: (callback: (jitsu: JitsuClient) => void | Promise<void>) => void
|
||||
}
|
||||
|
||||
const emptyClient: TelemetryClient = {withJitsu: () => {}};
|
||||
|
||||
function useTelemetry(): TelemetryClient {
|
||||
return useContext(TelemetryContext);
|
||||
}
|
||||
|
||||
function createTelemetryClient(): TelemetryClient {
|
||||
if (process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
|
||||
return {
|
||||
withJitsu: (callback) => {
|
||||
if (!process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
|
||||
//telemetry is disabled
|
||||
return;
|
||||
}
|
||||
if (!window) {
|
||||
console.warn("Jitsu has been called during SSR, this scenario isn't supported yet");
|
||||
return;
|
||||
} else if (!window['jitsu']) {
|
||||
window['jitsu'] = jitsuClient({
|
||||
log_level: 'ERROR',
|
||||
tracking_host: "https://t.calendso.com",
|
||||
key: "js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r",
|
||||
cookie_name: "__clnds",
|
||||
capture_3rd_party_cookies: false,
|
||||
});
|
||||
}
|
||||
let res = callback(window['jitsu']);
|
||||
if (res && typeof res['catch'] === "function") {
|
||||
res.catch(e => {
|
||||
console.debug("Unable to send telemetry event", e)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return emptyClient;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const TelemetryContext = React.createContext<TelemetryClient>(emptyClient)
|
||||
|
||||
const TelemetryProvider = TelemetryContext.Provider
|
||||
|
||||
export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry };
|
|
@ -1,5 +1,20 @@
|
|||
const withTM = require('next-transpile-modules')(['react-timezone-select']);
|
||||
|
||||
const validJson = (jsonString) => {
|
||||
try {
|
||||
const o = JSON.parse(jsonString);
|
||||
if (o && typeof o === "object") {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
catch (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.");
|
||||
}
|
||||
|
||||
module.exports = withTM({
|
||||
future: {
|
||||
webpack5: true,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@jitsu/sdk-js": "^2.0.1",
|
||||
"@prisma/client": "2.21.2",
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
|
|
@ -3,19 +3,20 @@ import Head from 'next/head';
|
|||
import Link from 'next/link';
|
||||
import prisma from '../../lib/prisma';
|
||||
import { useRouter } from 'next/router';
|
||||
const dayjs = require('dayjs');
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { ClockIcon, GlobeIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
|
||||
const isSameOrBefore = require('dayjs/plugin/isSameOrBefore');
|
||||
const isBetween = require('dayjs/plugin/isBetween');
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
const timezone = require('dayjs/plugin/timezone');
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
import getSlots from '../../lib/slots';
|
||||
import {useTelemetry} from "../../lib/telemetry";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
|
@ -23,12 +24,13 @@ function classNames(...classes) {
|
|||
|
||||
export default function Type(props) {
|
||||
// Initialise state
|
||||
const [selectedDate, setSelectedDate] = useState('');
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||
const [selectedMonth, setSelectedMonth] = useState(dayjs().month());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||
const [is24h, setIs24h] = useState(false);
|
||||
const [busy, setBusy] = useState([]);
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
// Get router variables
|
||||
const router = useRouter();
|
||||
|
@ -67,10 +69,9 @@ export default function Type(props) {
|
|||
days.push(i);
|
||||
}
|
||||
|
||||
|
||||
// Create placeholder elements for empty days in first week
|
||||
const weekdayOfFirst = dayjs().month(selectedMonth).date(1).day();
|
||||
const emptyDays = Array(weekdayOfFirst).fill(null).map((day, i) =>
|
||||
const emptyDays = Array(weekdayOfFirst).fill(null).map((day, i) =>
|
||||
<div key={`e-${i}`} className={"text-center w-10 h-10 rounded-full mx-auto"}>
|
||||
{null}
|
||||
</div>
|
||||
|
@ -78,7 +79,10 @@ export default function Type(props) {
|
|||
|
||||
// Combine placeholder days with actual days
|
||||
const calendar = [...emptyDays, ...days.map((day) =>
|
||||
<button key={day} onClick={(e) => setSelectedDate(dayjs().tz(dayjs.tz.guess()).month(selectedMonth).date(day))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}>
|
||||
<button key={day} onClick={(e) => {
|
||||
telemetry.withJitsu((jitsu) => jitsu.track('date_selected', {page_title: "", source_ip: ""}))
|
||||
setSelectedDate(dayjs().tz(dayjs.tz.guess()).month(selectedMonth).date(day))
|
||||
}} disabled={selectedMonth < parseInt(dayjs().format('MM')) && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}>
|
||||
{day}
|
||||
</button>
|
||||
)];
|
||||
|
@ -89,7 +93,7 @@ export default function Type(props) {
|
|||
if (!selectedDate) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
setLoading(true);
|
||||
const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`);
|
||||
const busyTimes = await res.json();
|
||||
|
@ -144,7 +148,7 @@ export default function Type(props) {
|
|||
</Head>
|
||||
|
||||
<main className={"mx-auto my-24 transition-max-width ease-in-out duration-500 " + (selectedDate ? 'max-w-6xl' : 'max-w-3xl')}>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg md:max-h-96">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="sm:flex px-4 py-5 sm:p-4">
|
||||
<div className={"pr-8 sm:border-r " + (selectedDate ? 'sm:w-1/3' : 'sm:w-1/2')}>
|
||||
{props.user.avatar && <img src={props.user.avatar} alt="Avatar" className="w-16 h-16 rounded-full mb-4"/>}
|
||||
|
@ -193,7 +197,7 @@ export default function Type(props) {
|
|||
<div className="flex text-gray-600 font-light text-xl mb-4 ml-2">
|
||||
<span className="w-1/2">{dayjs().month(selectedMonth).format("MMMM YYYY")}</span>
|
||||
<div className="w-1/2 text-right">
|
||||
<button onClick={decrementMonth} className={"mr-4 " + (selectedMonth < dayjs().format('MM') && 'text-gray-400')} disabled={selectedMonth < dayjs().format('MM')}>
|
||||
<button onClick={decrementMonth} className={"mr-4 " + (selectedMonth < parseInt(dayjs().format('MM')) && 'text-gray-400')} disabled={selectedMonth < parseInt(dayjs().format('MM'))}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={incrementMonth}>
|
||||
|
@ -212,7 +216,7 @@ export default function Type(props) {
|
|||
{calendar}
|
||||
</div>
|
||||
</div>
|
||||
{selectedDate && <div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-96 overflow-y-scroll">
|
||||
{selectedDate && <div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-97 overflow-y-auto">
|
||||
<div className="text-gray-600 font-light text-xl mb-4 text-left">
|
||||
<span className="w-1/2">{dayjs(selectedDate).format("dddd DD MMMM YYYY")}</span>
|
||||
</div>
|
||||
|
|
|
@ -3,14 +3,21 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import { ClockIcon, CalendarIcon } from '@heroicons/react/solid';
|
||||
import prisma from '../../lib/prisma';
|
||||
import {useTelemetry} from "../../lib/telemetry";
|
||||
import {useEffect} from "react";
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
export default function Book(props) {
|
||||
const router = useRouter();
|
||||
const { date, user } = router.query;
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu(jitsu => jitsu.track('time_selected', { page_title: "", source_ip: "" }));
|
||||
})
|
||||
|
||||
const bookingHandler = event => {
|
||||
event.preventDefault();
|
||||
telemetry.withJitsu(jitsu => jitsu.track('booking_confirmed', { page_title: "", source_ip: "" }));
|
||||
const res = fetch(
|
||||
'/api/book/' + user,
|
||||
{
|
||||
|
@ -59,13 +66,13 @@ export default function Book(props) {
|
|||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
|
||||
<div className="mt-1">
|
||||
<input type="text" name="name" id="name" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Doe" />
|
||||
<input type="text" name="name" id="name" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Doe" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
|
||||
<div className="mt-1">
|
||||
<input type="text" name="email" id="email" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" />
|
||||
<input type="email" name="email" id="email" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import '../styles/globals.css';
|
||||
import {createTelemetryClient, TelemetryProvider} from '../lib/telemetry';
|
||||
import { Provider } from 'next-auth/client';
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider session={pageProps.session}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
<TelemetryProvider value={createTelemetryClient()}>
|
||||
<Provider session={pageProps.session}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
</TelemetryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useSession, getSession } from 'next-auth/client';
|
|||
import { CheckCircleIcon, XCircleIcon, ChevronRightIcon, PlusIcon } from '@heroicons/react/solid';
|
||||
import { InformationCircleIcon } from '@heroicons/react/outline';
|
||||
|
||||
export default function Home(props) {
|
||||
export default function Home({ integrations }) {
|
||||
const [session, loading] = useSession();
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
|
||||
|
@ -24,7 +24,7 @@ export default function Home(props) {
|
|||
}
|
||||
|
||||
function integrationHandler(type) {
|
||||
fetch('/api/integrations/' + type + '/add')
|
||||
fetch('/api/integrations/' + type.replace('_', '') + '/add')
|
||||
.then((response) => response.json())
|
||||
.then((data) => window.location.href = data.url);
|
||||
}
|
||||
|
@ -38,73 +38,63 @@ export default function Home(props) {
|
|||
|
||||
<Shell heading="Integrations">
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{props.credentials.map((integration) =>
|
||||
<li>
|
||||
<Link href={"/integrations/" + integration.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">
|
||||
{integration.type == 'google_calendar' && <img className="h-10 w-10 mr-2" src="integrations/google-calendar.png" alt="Google Calendar" />}
|
||||
{integration.type == 'office365_calendar' && <img className="h-10 w-10 mr-2" src="integrations/office-365.png" alt="Office 365 / Outlook.com Calendar" />}
|
||||
{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>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
|
||||
<div>
|
||||
{integration.type == 'google_calendar' && <p className="text-sm font-medium text-blue-600 truncate">Google Calendar</p>}
|
||||
{integration.type == 'office365_calendar' && <p className="text-sm font-medium text-blue-600 truncate">Office365 / Outlook.com Calendar</p>}
|
||||
<p className="flex items-center text-sm text-gray-500">
|
||||
{integration.type.endsWith('_calendar') && <span className="truncate">Calendar Integration</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<div>
|
||||
{integration.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>
|
||||
}
|
||||
{!integration.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 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>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>))}
|
||||
</ul>
|
||||
{props.credentials.length == 0 &&
|
||||
<div className="bg-white shadow sm:rounded-lg">
|
||||
<div className="flex">
|
||||
<div className="py-9 pl-8">
|
||||
<InformationCircleIcon className="text-blue-600 w-16" />
|
||||
:
|
||||
<div className="bg-white shadow sm: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="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">→</span></button>
|
||||
</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">→</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{showAddModal &&
|
||||
|
@ -150,30 +140,18 @@ export default function Home(props) {
|
|||
</div>
|
||||
<div className="my-4">
|
||||
<ul className="divide-y divide-gray-200">
|
||||
<li className="flex py-4">
|
||||
{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="integrations/office-365.png" alt="Office 365 / Outlook.com Calendar" />
|
||||
<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">Office 365 / Outlook.com Calendar</h2>
|
||||
<p className="text-gray-400 text-sm">For personal and business accounts</p>
|
||||
<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('office365calendar')} className="font-medium text-blue-600 hover:text-blue-500">Add</button>
|
||||
<button onClick={() => integrationHandler(integration.type)} className="font-medium text-blue-600 hover:text-blue-500">Add</button>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex py-4">
|
||||
<div className="w-1/12 mr-4 pt-2">
|
||||
<img className="h-8 w-8 mr-2" src="integrations/google-calendar.png" alt="Google Calendar" />
|
||||
</div>
|
||||
<div className="w-10/12">
|
||||
<h2 className="text-gray-800 font-medium">Google Calendar</h2>
|
||||
<p className="text-gray-400 text-sm">For personal and business accounts</p>
|
||||
</div>
|
||||
<div className="w-2/12 text-right pt-2">
|
||||
<button onClick={() => integrationHandler('googlecalendar')} className="font-medium text-blue-600 hover:text-blue-500">Add</button>
|
||||
</div>
|
||||
</li>
|
||||
</li>))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
|
@ -190,6 +168,17 @@ export default function Home(props) {
|
|||
);
|
||||
}
|
||||
|
||||
const validJson = (jsonString: string) => {
|
||||
try {
|
||||
const o = JSON.parse(jsonString);
|
||||
if (o && typeof o === "object") {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context);
|
||||
|
||||
|
@ -212,7 +201,24 @@ export async function getServerSideProps(context) {
|
|||
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 accounts",
|
||||
}, {
|
||||
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 accounts",
|
||||
} ];
|
||||
|
||||
return {
|
||||
props: {credentials}, // will be passed to the page component as props
|
||||
props: {integrations},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,9 +74,6 @@ export default function Settings(props) {
|
|||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<button type="button" className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save
|
||||
</button>
|
||||
|
|
|
@ -145,9 +145,6 @@ export default function Settings(props) {
|
|||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<button type="button" className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save
|
||||
</button>
|
||||
|
@ -181,4 +178,4 @@ export async function getServerSideProps(context) {
|
|||
return {
|
||||
props: {user}, // will be passed to the page component as props
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,7 +74,7 @@ export default function Success(props) {
|
|||
<div className="mt-5 sm:mt-6 text-center">
|
||||
<span className="font-medium text-gray-500">Add to your calendar</span>
|
||||
<div className="flex mt-2">
|
||||
<Link href={encodeURI("https://calendar.google.com/calendar/render?action=TEMPLATE&dates=" + dayjs(date).format() + "%2F" + dayjs(date).add(props.eventType.length, 'minute').format() + "&details=" + props.eventType.title + " with " + props.user.name + "&text=" + props.eventType.description)}>
|
||||
<Link href={`https://calendar.google.com/calendar/r/eventedit?dates=${dayjs(date).format('YYYYMMDDTHHmmss[Z]')}/${dayjs(date).add(props.eventType.length, 'minute').format('YYYYMMDDTHHmmss[Z]')}&text=${props.eventType.title} with ${props.user.name}&details=${props.eventType.description}`}>
|
||||
<a className="mx-2 btn-wide btn-white">
|
||||
<svg className="inline-block w-4 h-4 mr-1 -mt-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>
|
||||
</a>
|
||||
|
|
|
@ -26,7 +26,10 @@ module.exports = {
|
|||
800: '#0277bd',
|
||||
900: '#01579b',
|
||||
},
|
||||
}
|
||||
},
|
||||
maxHeight: {
|
||||
97: '25rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
|
|
Loading…
Reference in a new issue