Fixed linting errors
This commit is contained in:
		
						commit
						e0abbbb2f1
					
				
					 7 changed files with 519 additions and 442 deletions
				
			
		|  | @ -1,9 +1,8 @@ | ||||||
| import { GiftIcon } from "@heroicons/react/outline"; | import { GiftIcon } from "@heroicons/react/outline"; | ||||||
| export default function DonateBanner() { | export default function DonateBanner() { | ||||||
| 
 |   if (location.hostname.endsWith(".calendso.com")) { | ||||||
| if (location.hostname.endsWith(".calendso.com")) { |     return null; | ||||||
| return null; |   } | ||||||
| }   |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|  | @ -17,21 +16,19 @@ return null; | ||||||
|                   <GiftIcon className="h-6 w-6 text-white" aria-hidden="true" /> |                   <GiftIcon className="h-6 w-6 text-white" aria-hidden="true" /> | ||||||
|                 </span> |                 </span> | ||||||
|                 <p className="ml-3 font-medium text-white truncate"> |                 <p className="ml-3 font-medium text-white truncate"> | ||||||
|                   <span className="md:hidden"> |                   <span className="md:hidden">Support the ongoing development</span> | ||||||
|                     Support the ongoing development |  | ||||||
|                   </span> |  | ||||||
|                   <span className="hidden md:inline"> |                   <span className="hidden md:inline"> | ||||||
|                     You're using the free self-hosted version. Support the |                     You're using the free self-hosted version. Support the ongoing development by making | ||||||
|                     ongoing development by making a donation. |                     a donation. | ||||||
|                   </span> |                   </span> | ||||||
|                 </p> |                 </p> | ||||||
|               </div> |               </div> | ||||||
|               <div className="order-3 mt-2 flex-shrink-0 w-full sm:order-2 sm:mt-0 sm:w-auto"> |               <div className="order-3 mt-2 flex-shrink-0 w-full sm:order-2 sm:mt-0 sm:w-auto"> | ||||||
|                 <a |                 <a | ||||||
|                   target="_blank" |                   target="_blank" | ||||||
|  |                   rel="noreferrer" | ||||||
|                   href="https://calendso.com/donate" |                   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 |                   Donate | ||||||
|                 </a> |                 </a> | ||||||
|               </div> |               </div> | ||||||
|  |  | ||||||
|  | @ -2,9 +2,6 @@ | ||||||
|   @apply font-medium; |   @apply font-medium; | ||||||
| } */ | } */ | ||||||
| 
 | 
 | ||||||
| .text { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .text--body { | .text--body { | ||||||
|   @apply text-lg leading-relaxed; |   @apply text-lg leading-relaxed; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,31 +1,40 @@ | ||||||
| 
 | import nextTranspileModules from "next-transpile-modules"; | ||||||
| const withTM = require('next-transpile-modules')(['react-timezone-select']); | const withTM = nextTranspileModules(["react-timezone-select"]); | ||||||
| 
 | 
 | ||||||
| // TODO: Revisit this later with getStaticProps in App
 | // TODO: Revisit this later with getStaticProps in App
 | ||||||
| if (process.env.NEXTAUTH_URL) { | 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 ) { | 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.'); |   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) { | 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) => { | const validJson = (jsonString) => { | ||||||
|     try { |   try { | ||||||
|         const o = JSON.parse(jsonString); |     const o = JSON.parse(jsonString); | ||||||
|         if (o && typeof o === "object") { |     if (o && typeof o === "object") { | ||||||
|             return o; |       return o; | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|     catch (e) {} |   } catch (e) { | ||||||
|     return false; |     console.error(e); | ||||||
| } |   } | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| if (process.env.GOOGLE_API_CREDENTIALS && ! validJson(process.env.GOOGLE_API_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."); |   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({ | module.exports = withTM({ | ||||||
|  | @ -38,10 +47,10 @@ module.exports = withTM({ | ||||||
|   async redirects() { |   async redirects() { | ||||||
|     return [ |     return [ | ||||||
|       { |       { | ||||||
|         source: '/settings', |         source: "/settings", | ||||||
|         destination: '/settings/profile', |         destination: "/settings/profile", | ||||||
|         permanent: true, |         permanent: true, | ||||||
|       } |       }, | ||||||
|     ] |     ]; | ||||||
|   } |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   if (req.method == "PATCH" || req.method == "POST") { |   if (req.method == "PATCH" || req.method == "POST") { | ||||||
|     const data = { |     const data = { | ||||||
|       title: req.body.title, |       title: req.body.title, | ||||||
|       slug: req.body.slug, |       slug: req.body.slug.trim(), | ||||||
|       description: req.body.description, |       description: req.body.description, | ||||||
|       length: parseInt(req.body.length), |       length: parseInt(req.body.length), | ||||||
|       hidden: req.body.hidden, |       hidden: req.body.hidden, | ||||||
|  |  | ||||||
|  | @ -1,41 +1,45 @@ | ||||||
| import Head from 'next/head'; | import Head from "next/head"; | ||||||
| import Link from 'next/link'; | import Link from "next/link"; | ||||||
| import { CheckIcon } from '@heroicons/react/outline'; | import { CheckIcon } from "@heroicons/react/outline"; | ||||||
| 
 | 
 | ||||||
| export default function Logout() { | export default function Logout() { | ||||||
|     return ( |   return ( | ||||||
|         <div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> |     <div | ||||||
|             <Head> |       className="fixed z-10 inset-0 overflow-y-auto" | ||||||
|                 <title>Logged out - Calendso</title> |       aria-labelledby="modal-title" | ||||||
|                 <link rel="icon" href="/favicon.ico" /> |       role="dialog" | ||||||
|             </Head> |       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"> |       <Head> | ||||||
|                 <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> |         <title>Logged out - Calendso</title> | ||||||
|                 <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"> |         <link rel="icon" href="/favicon.ico" /> | ||||||
|                     <div> |       </Head> | ||||||
|                         <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100"> |       <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | ||||||
|                             <CheckIcon className="h-6 w-6 text-green-600" /> |         <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> | ||||||
|                         </div> |           ​ | ||||||
|                         <div className="mt-3 text-center sm:mt-5"> |         </span> | ||||||
|                             <h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> |         <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"> | ||||||
|                                 You've been logged out |           <div> | ||||||
|                             </h3> |             <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100"> | ||||||
|                             <div className="mt-2"> |               <CheckIcon className="h-6 w-6 text-green-600" /> | ||||||
|                                 <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> | ||||||
|  |             <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> |         </div> | ||||||
|     ); |       </div> | ||||||
| } |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -155,7 +155,7 @@ export default function Availability(props) { | ||||||
|                   </thead> |                   </thead> | ||||||
|                   <tbody className="bg-white divide-y divide-gray-200"> |                   <tbody className="bg-white divide-y divide-gray-200"> | ||||||
|                     {props.types.map((eventType) => ( |                     {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"> |                         <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 align-top"> | ||||||
|                           {eventType.title} |                           {eventType.title} | ||||||
|                           {eventType.hidden && ( |                           {eventType.hidden && ( | ||||||
|  | @ -341,7 +341,7 @@ export default function Availability(props) { | ||||||
|                           Hide this event type |                           Hide this event type | ||||||
|                         </label> |                         </label> | ||||||
|                         <p className="text-gray-500"> |                         <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's URL. | ||||||
|                         </p> |                         </p> | ||||||
|                       </div> |                       </div> | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
|  | @ -1,387 +1,457 @@ | ||||||
| import Head from 'next/head'; | import Head from "next/head"; | ||||||
| import Link from 'next/link'; | import Link from "next/link"; | ||||||
| import prisma from '../../lib/prisma'; | import prisma from "../../lib/prisma"; | ||||||
| import Shell from '../../components/Shell'; | import Shell from "../../components/Shell"; | ||||||
| import {useEffect, useState} from 'react'; | import { useEffect, useState } from "react"; | ||||||
| import {getSession, useSession} from 'next-auth/client'; | import { getSession, useSession } from "next-auth/client"; | ||||||
| import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; | import { | ||||||
| import {InformationCircleIcon} from '@heroicons/react/outline'; |   CalendarIcon, | ||||||
| import {Switch} from '@headlessui/react' |   CheckCircleIcon, | ||||||
|  |   ChevronRightIcon, | ||||||
|  |   PlusIcon, | ||||||
|  |   XCircleIcon, | ||||||
|  | } from "@heroicons/react/solid"; | ||||||
|  | import { InformationCircleIcon } from "@heroicons/react/outline"; | ||||||
|  | import { Switch } from "@headlessui/react"; | ||||||
| 
 | 
 | ||||||
| export default function Home({ integrations }) { | export default function Home({ integrations }) { | ||||||
|     const [session, loading] = useSession(); |   const [, loading] = useSession(); | ||||||
|     const [showAddModal, setShowAddModal] = useState(false); |   const [showAddModal, setShowAddModal] = useState(false); | ||||||
|     const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); |   const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); | ||||||
|     const [selectableCalendars, setSelectableCalendars] = useState([]); |   const [selectableCalendars, setSelectableCalendars] = useState([]); | ||||||
| 
 | 
 | ||||||
|     function toggleAddModal() { |   function toggleAddModal() { | ||||||
|         setShowAddModal(!showAddModal); |     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() { |   function classNames(...classes) { | ||||||
|         setShowSelectCalendarModal(!showSelectCalendarModal); |     return classes.filter(Boolean).join(" "); | ||||||
|     } |   } | ||||||
| 
 | 
 | ||||||
|     function loadCalendars() { |   useEffect(loadCalendars, [integrations]); | ||||||
|         fetch('api/availability/calendar') |  | ||||||
|           .then((response) => response.json()) |  | ||||||
|           .then(data => { |  | ||||||
|               setSelectableCalendars(data) |  | ||||||
|           }); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     function integrationHandler(type) { |   if (loading) { | ||||||
|         fetch('/api/integrations/' + type.replace('_', '') + '/add') |     return <div className="loader"></div>; | ||||||
|             .then((response) => response.json()) |   } | ||||||
|             .then((data) => window.location.href = data.url); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     function calendarSelectionHandler(calendar) { |   return ( | ||||||
|         return (selected) => { |     <div> | ||||||
|             let cals = [...selectableCalendars]; |       <Head> | ||||||
|             let i = cals.findIndex(c => c.externalId === calendar.externalId); |         <title>Integrations | Calendso</title> | ||||||
|             cals[i].selected = selected; |         <link rel="icon" href="/favicon.ico" /> | ||||||
|             setSelectableCalendars(cals); |       </Head> | ||||||
|             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){ |       <Shell heading="Integrations" noPaddingBottom> | ||||||
|         switch (integrationType) { |         <div className="text-right py-2"> | ||||||
|             case "google_calendar": return "integrations/google-calendar.png"; |           <button | ||||||
|             case "office365_calendar": return "integrations/office-365.png"; |             onClick={toggleAddModal} | ||||||
|             default: return ""; |             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> | ||||||
|     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">→</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">​</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">​</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> |  | ||||||
|         </div> |         </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'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> | ||||||
|  |               </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"> | ||||||
|  |                 ​ | ||||||
|  |               </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"> | ||||||
|  |                 ​ | ||||||
|  |               </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) => { | const validJson = (jsonString: string) => { | ||||||
|     try { |   try { | ||||||
|         const o = JSON.parse(jsonString); |     const o = JSON.parse(jsonString); | ||||||
|         if (o && typeof o === "object") { |     if (o && typeof o === "object") { | ||||||
|             return o; |       return o; | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|     catch (e) {} |   } catch (e) { | ||||||
|     return false; |     console.error(e); | ||||||
| } |   } | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export async function getServerSideProps(context) { | export async function getServerSideProps(context) { | ||||||
|     const session = await getSession(context); |   const session = await getSession(context); | ||||||
|     if (!session) { |   if (!session) { | ||||||
|         return { redirect: { permanent: false, destination: '/auth/login' } }; |     return { redirect: { permanent: false, destination: "/auth/login" } }; | ||||||
|     } |   } | ||||||
|     const user = await prisma.user.findFirst({ |   const user = await prisma.user.findFirst({ | ||||||
|         where: { |     where: { | ||||||
|             email: session.user.email, |       email: session.user.email, | ||||||
|         }, |     }, | ||||||
|         select: { |     select: { | ||||||
|             id: true |       id: true, | ||||||
|         } |     }, | ||||||
|     }); |   }); | ||||||
| 
 | 
 | ||||||
|     const credentials = await prisma.credential.findMany({ |   const credentials = await prisma.credential.findMany({ | ||||||
|         where: { |     where: { | ||||||
|             userId: user.id, |       userId: user.id, | ||||||
|         }, |     }, | ||||||
|         select: { |     select: { | ||||||
|             id: true, |       id: true, | ||||||
|             type: true, |       type: true, | ||||||
|             key: true |       key: true, | ||||||
|         } |     }, | ||||||
|     }); |   }); | ||||||
| 
 | 
 | ||||||
|     const integrations = [ { |   const integrations = [ | ||||||
|         installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), |     { | ||||||
|         credential: credentials.find( (integration) => integration.type === "google_calendar" ) || null, |       installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), | ||||||
|         type: "google_calendar", |       credential: credentials.find((integration) => integration.type === "google_calendar") || null, | ||||||
|         title: "Google Calendar", |       type: "google_calendar", | ||||||
|         imageSrc: "integrations/google-calendar.png", |       title: "Google Calendar", | ||||||
|         description: "For personal and business calendars", |       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, |       installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), | ||||||
|         title: "Office 365 / Outlook.com Calendar", |       type: "office365_calendar", | ||||||
|         imageSrc: "integrations/office-365.png", |       credential: credentials.find((integration) => integration.type === "office365_calendar") || null, | ||||||
|         description: "For personal and business calendars", |       title: "Office 365 / Outlook.com Calendar", | ||||||
|     }, { |       imageSrc: "integrations/office-365.png", | ||||||
|         installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET), |       description: "For personal and business calendars", | ||||||
|         type: "zoom_video", |     }, | ||||||
|         credential: credentials.find( (integration) => integration.type === "zoom_video" ) || null, |     { | ||||||
|         title: "Zoom", |       installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET), | ||||||
|         imageSrc: "integrations/zoom.png", |       type: "zoom_video", | ||||||
|         description: "Video Conferencing", |       credential: credentials.find((integration) => integration.type === "zoom_video") || null, | ||||||
|     } ]; |       title: "Zoom", | ||||||
|  |       imageSrc: "integrations/zoom.png", | ||||||
|  |       description: "Video Conferencing", | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
| 
 | 
 | ||||||
|     return { |   return { | ||||||
|       props: {integrations}, |     props: { integrations }, | ||||||
|     } |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas