Added zoom as an event location and fixed ESLint
This commit is contained in:
		
							parent
							
								
									dada6a3a79
								
							
						
					
					
						commit
						bc47975316
					
				
					 3 changed files with 1048 additions and 764 deletions
				
			
		|  | @ -1,7 +1,6 @@ | |||
| 
 | ||||
| export enum LocationType { | ||||
|     InPerson = 'inPerson', | ||||
|     Phone = 'phone', | ||||
|     GoogleMeet = 'integrations:google:meet' | ||||
|   InPerson = "inPerson", | ||||
|   Phone = "phone", | ||||
|   GoogleMeet = "integrations:google:meet", | ||||
|   Zoom = "integrations:zoom", | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,71 +1,73 @@ | |||
| import Head from 'next/head'; | ||||
| import Link from 'next/link'; | ||||
| import {useRouter} from 'next/router'; | ||||
| import {CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon} from '@heroicons/react/solid'; | ||||
| import prisma from '../../lib/prisma'; | ||||
| import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry"; | ||||
| import {useEffect, useState} from "react"; | ||||
| import dayjs from 'dayjs'; | ||||
| import utc from 'dayjs/plugin/utc'; | ||||
| import timezone from 'dayjs/plugin/timezone'; | ||||
| import 'react-phone-number-input/style.css'; | ||||
| import PhoneInput from 'react-phone-number-input'; | ||||
| import {LocationType} from '../../lib/location'; | ||||
| import Avatar from '../../components/Avatar'; | ||||
| import Button from '../../components/ui/Button'; | ||||
| import {EventTypeCustomInputType} from "../../lib/eventTypeInput"; | ||||
| import Head from "next/head"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid"; | ||||
| import prisma from "../../lib/prisma"; | ||||
| import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import dayjs from "dayjs"; | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import "react-phone-number-input/style.css"; | ||||
| import PhoneInput from "react-phone-number-input"; | ||||
| import { LocationType } from "../../lib/location"; | ||||
| import Avatar from "../../components/Avatar"; | ||||
| import Button from "../../components/ui/Button"; | ||||
| import { EventTypeCustomInputType } from "../../lib/eventTypeInput"; | ||||
| 
 | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| 
 | ||||
| export default function Book(props) { | ||||
| export default function Book(props: any): JSX.Element { | ||||
|   const router = useRouter(); | ||||
|   const { date, user, rescheduleUid } = router.query; | ||||
| 
 | ||||
|     const [ is24h, setIs24h ] = useState(false); | ||||
|     const [ preferredTimeZone, setPreferredTimeZone ] = useState(''); | ||||
|     const [ loading, setLoading ] = useState(false); | ||||
|     const [ error, setError ] = useState(false); | ||||
|   const [is24h, setIs24h] = useState(false); | ||||
|   const [preferredTimeZone, setPreferredTimeZone] = useState(""); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [error, setError] = useState(false); | ||||
| 
 | ||||
|   const locations = props.eventType.locations || []; | ||||
| 
 | ||||
|     const [ selectedLocation, setSelectedLocation ] = useState<LocationType>(locations.length === 1 ? locations[0].type : ''); | ||||
|   const [selectedLocation, setSelectedLocation] = useState<LocationType>( | ||||
|     locations.length === 1 ? locations[0].type : "" | ||||
|   ); | ||||
|   const telemetry = useTelemetry(); | ||||
|   useEffect(() => { | ||||
|     setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()); | ||||
|     setIs24h(!!localStorage.getItem("timeOption.is24hClock")); | ||||
| 
 | ||||
|         setPreferredTimeZone(localStorage.getItem('timeOption.preferredTimeZone') || dayjs.tz.guess()); | ||||
|         setIs24h(!!localStorage.getItem('timeOption.is24hClock')); | ||||
| 
 | ||||
|         telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters())); | ||||
|     telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters())); | ||||
|   }); | ||||
| 
 | ||||
|     const locationInfo = (type: LocationType) => locations.find( | ||||
|         (location) => location.type === type | ||||
|     ); | ||||
|   const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); | ||||
| 
 | ||||
|   // TODO: Move to translations
 | ||||
|   const locationLabels = { | ||||
|         [LocationType.InPerson]: 'In-person meeting', | ||||
|         [LocationType.Phone]: 'Phone call', | ||||
|         [LocationType.GoogleMeet]: 'Google Meet', | ||||
|     [LocationType.InPerson]: "In-person meeting", | ||||
|     [LocationType.Phone]: "Phone call", | ||||
|     [LocationType.GoogleMeet]: "Google Meet", | ||||
|     [LocationType.Zoom]: "Zoom Video", | ||||
|   }; | ||||
| 
 | ||||
|     const bookingHandler = event => { | ||||
|   const bookingHandler = (event) => { | ||||
|     const book = async () => { | ||||
|       setLoading(true); | ||||
|       setError(false); | ||||
|       let notes = ""; | ||||
|       if (props.eventType.customInputs) { | ||||
|                 notes = props.eventType.customInputs.map(input => { | ||||
|         notes = props.eventType.customInputs | ||||
|           .map((input) => { | ||||
|             const data = event.target["custom_" + input.id]; | ||||
|                     if (!!data) { | ||||
|             if (data) { | ||||
|               if (input.type === EventTypeCustomInputType.Bool) { | ||||
|                             return input.label + "\n" + (data.value ? "Yes" : "No") | ||||
|                 return input.label + "\n" + (data.value ? "Yes" : "No"); | ||||
|               } else { | ||||
|                             return input.label + "\n" + data.value | ||||
|                 return input.label + "\n" + data.value; | ||||
|               } | ||||
|             } | ||||
|                 }).join("\n\n") | ||||
|           }) | ||||
|           .join("\n\n"); | ||||
|       } | ||||
|       if (!!notes && !!event.target.notes.value) { | ||||
|         notes += "\n\nAdditional notes:\n" + event.target.notes.value; | ||||
|  | @ -73,54 +75,54 @@ export default function Book(props) { | |||
|         notes += event.target.notes.value; | ||||
|       } | ||||
| 
 | ||||
|             let payload = { | ||||
|       const payload = { | ||||
|         start: dayjs(date).format(), | ||||
|                 end: dayjs(date).add(props.eventType.length, 'minute').format(), | ||||
|         end: dayjs(date).add(props.eventType.length, "minute").format(), | ||||
|         name: event.target.name.value, | ||||
|         email: event.target.email.value, | ||||
|         notes: notes, | ||||
|         timeZone: preferredTimeZone, | ||||
|         eventTypeId: props.eventType.id, | ||||
|                 rescheduleUid: rescheduleUid | ||||
|         rescheduleUid: rescheduleUid, | ||||
|       }; | ||||
| 
 | ||||
|       if (selectedLocation) { | ||||
|         switch (selectedLocation) { | ||||
|           case LocationType.Phone: | ||||
|                     payload['location'] = event.target.phone.value | ||||
|                     break | ||||
|             payload["location"] = event.target.phone.value; | ||||
|             break; | ||||
| 
 | ||||
|           case LocationType.InPerson: | ||||
|                     payload['location'] = locationInfo(selectedLocation).address | ||||
|                     break | ||||
|             payload["location"] = locationInfo(selectedLocation).address; | ||||
|             break; | ||||
| 
 | ||||
|                 case LocationType.GoogleMeet: | ||||
|                     payload['location'] = LocationType.GoogleMeet | ||||
|                 break | ||||
|           // Catches all other location types, such as Google Meet, Zoom etc.
 | ||||
|           default: | ||||
|             payload["location"] = selectedLocation; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|             telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); | ||||
|       telemetry.withJitsu((jitsu) => | ||||
|         jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters()) | ||||
|       ); | ||||
| 
 | ||||
|             /*const res = await */fetch( | ||||
|               '/api/book/' + user, | ||||
|               { | ||||
|       /*const res = await */ fetch("/api/book/" + user, { | ||||
|         body: JSON.stringify(payload), | ||||
|         headers: { | ||||
|                       'Content-Type': 'application/json' | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|                   method: 'POST' | ||||
|               } | ||||
|             ); | ||||
|         method: "POST", | ||||
|       }); | ||||
|       // TODO When the endpoint is fixed, change this to await the result again
 | ||||
|       //if (res.ok) {
 | ||||
|                 let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; | ||||
|                 if (payload['location']) { | ||||
|                     if (payload['location'].includes('integration')) { | ||||
|       let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${ | ||||
|         props.user.username | ||||
|       }&reschedule=${!!rescheduleUid}&name=${payload.name}`;
 | ||||
|       if (payload["location"]) { | ||||
|         if (payload["location"].includes("integration")) { | ||||
|           successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); | ||||
|                     } | ||||
|                     else { | ||||
|                         successUrl += "&location=" + encodeURIComponent(payload['location']); | ||||
|         } else { | ||||
|           successUrl += "&location=" + encodeURIComponent(payload["location"]); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|  | @ -129,16 +131,19 @@ export default function Book(props) { | |||
|                 setLoading(false); | ||||
|                 setError(true); | ||||
|             }*/ | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     event.preventDefault(); | ||||
|     book(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <Head> | ||||
|                 <title>{rescheduleUid ? 'Reschedule' : 'Confirm'} your {props.eventType.title} with {props.user.name || props.user.username} | Calendso</title> | ||||
|         <title> | ||||
|           {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} | ||||
|           {props.user.name || props.user.username} | Calendso | ||||
|         </title> | ||||
|         <link rel="icon" href="/favicon.ico" /> | ||||
|       </Head> | ||||
| 
 | ||||
|  | @ -153,108 +158,202 @@ export default function Book(props) { | |||
|                 <ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> | ||||
|                 {props.eventType.length} minutes | ||||
|               </p> | ||||
|                             {selectedLocation === LocationType.InPerson && <p className="text-gray-500 mb-2"> | ||||
|               {selectedLocation === LocationType.InPerson && ( | ||||
|                 <p className="text-gray-500 mb-2"> | ||||
|                   <LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> | ||||
|                   {locationInfo(selectedLocation).address} | ||||
|                             </p>} | ||||
|                 </p> | ||||
|               )} | ||||
|               <p className="text-blue-600 mb-4"> | ||||
|                 <CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> | ||||
|                                 {preferredTimeZone && dayjs(date).tz(preferredTimeZone).format( (is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} | ||||
|                 {preferredTimeZone && | ||||
|                   dayjs(date) | ||||
|                     .tz(preferredTimeZone) | ||||
|                     .format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} | ||||
|               </p> | ||||
|               <p className="text-gray-600">{props.eventType.description}</p> | ||||
|             </div> | ||||
|             <div className="sm:w-1/2 pl-8 pr-4"> | ||||
|               <form onSubmit={bookingHandler}> | ||||
|                 <div className="mb-4"> | ||||
|                                     <label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label> | ||||
|                   <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" 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" defaultValue={props.booking ? props.booking.attendees[0].name : ''} /> | ||||
|                     <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" | ||||
|                       defaultValue={props.booking ? props.booking.attendees[0].name : ""} | ||||
|                     /> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div className="mb-4"> | ||||
|                                     <label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label> | ||||
|                   <label htmlFor="email" className="block text-sm font-medium text-gray-700"> | ||||
|                     Email address | ||||
|                   </label> | ||||
|                   <div className="mt-1"> | ||||
|                                         <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" defaultValue={props.booking ? props.booking.attendees[0].email : ''} /> | ||||
|                     <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" | ||||
|                       defaultValue={props.booking ? props.booking.attendees[0].email : ""} | ||||
|                     /> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 {locations.length > 1 && ( | ||||
|                   <div className="mb-4"> | ||||
|                     <span className="block text-sm font-medium text-gray-700">Location</span> | ||||
|                                         {locations.map( (location) => ( | ||||
|                     {locations.map((location) => ( | ||||
|                       <label key={location.type} className="block"> | ||||
|                                                 <input type="radio" required onChange={(e) => setSelectedLocation(e.target.value)} className="location" name="location" value={location.type} checked={selectedLocation === location.type} /> | ||||
|                         <input | ||||
|                           type="radio" | ||||
|                           required | ||||
|                           onChange={(e) => setSelectedLocation(e.target.value)} | ||||
|                           className="location" | ||||
|                           name="location" | ||||
|                           value={location.type} | ||||
|                           checked={selectedLocation === location.type} | ||||
|                         /> | ||||
|                         <span className="text-sm ml-2">{locationLabels[location.type]}</span> | ||||
|                       </label> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 )} | ||||
|                                 {selectedLocation === LocationType.Phone && (<div className="mb-4"> | ||||
|                                    <label htmlFor="phone" className="block text-sm font-medium text-gray-700">Phone Number</label> | ||||
|                                    <div className="mt-1"> | ||||
|                                        <PhoneInput name="phone" placeholder="Enter phone number" id="phone" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" onChange={() => {}} /> | ||||
|                                    </div> | ||||
|                                 </div>)} | ||||
|                                 {props.eventType.customInputs && props.eventType.customInputs.sort((a,b) => a.id - b.id).map(input => ( | ||||
|                 {selectedLocation === LocationType.Phone && ( | ||||
|                   <div className="mb-4"> | ||||
|                                       {input.type !== EventTypeCustomInputType.Bool && | ||||
|                                       <label htmlFor={input.label} className="block text-sm font-medium text-gray-700 mb-1">{input.label}</label>} | ||||
|                                       {input.type === EventTypeCustomInputType.TextLong && | ||||
|                                       <textarea name={"custom_" + input.id} id={"custom_" + input.id} | ||||
|                     <label htmlFor="phone" className="block text-sm font-medium text-gray-700"> | ||||
|                       Phone Number | ||||
|                     </label> | ||||
|                     <div className="mt-1"> | ||||
|                       <PhoneInput | ||||
|                         name="phone" | ||||
|                         placeholder="Enter phone number" | ||||
|                         id="phone" | ||||
|                         required | ||||
|                         className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 )} | ||||
|                 {props.eventType.customInputs && | ||||
|                   props.eventType.customInputs | ||||
|                     .sort((a, b) => a.id - b.id) | ||||
|                     .map((input) => ( | ||||
|                       <div className="mb-4" key={"input-" + input.label.toLowerCase}> | ||||
|                         {input.type !== EventTypeCustomInputType.Bool && ( | ||||
|                           <label | ||||
|                             htmlFor={input.label} | ||||
|                             className="block text-sm font-medium text-gray-700 mb-1"> | ||||
|                             {input.label} | ||||
|                           </label> | ||||
|                         )} | ||||
|                         {input.type === EventTypeCustomInputType.TextLong && ( | ||||
|                           <textarea | ||||
|                             name={"custom_" + input.id} | ||||
|                             id={"custom_" + input.id} | ||||
|                             required={input.required} | ||||
|                             rows={3} | ||||
|                             className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                                 placeholder=""/>} | ||||
|                                       {input.type === EventTypeCustomInputType.Text && | ||||
|                                       <input type="text" name={"custom_" + input.id} id={"custom_" + input.id} | ||||
|                             placeholder="" | ||||
|                           /> | ||||
|                         )} | ||||
|                         {input.type === EventTypeCustomInputType.Text && ( | ||||
|                           <input | ||||
|                             type="text" | ||||
|                             name={"custom_" + input.id} | ||||
|                             id={"custom_" + input.id} | ||||
|                             required={input.required} | ||||
|                             className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                              placeholder=""/>} | ||||
|                                       {input.type === EventTypeCustomInputType.Number && | ||||
|                                       <input type="number" name={"custom_" + input.id} id={"custom_" + input.id} | ||||
|                             placeholder="" | ||||
|                           /> | ||||
|                         )} | ||||
|                         {input.type === EventTypeCustomInputType.Number && ( | ||||
|                           <input | ||||
|                             type="number" | ||||
|                             name={"custom_" + input.id} | ||||
|                             id={"custom_" + input.id} | ||||
|                             required={input.required} | ||||
|                             className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                              placeholder=""/>} | ||||
|                                       {input.type === EventTypeCustomInputType.Bool && | ||||
|                             placeholder="" | ||||
|                           /> | ||||
|                         )} | ||||
|                         {input.type === EventTypeCustomInputType.Bool && ( | ||||
|                           <div className="flex items-center h-5"> | ||||
|                                           <input type="checkbox" name={"custom_" + input.id} id={"custom_" + input.id} | ||||
|                             <input | ||||
|                               type="checkbox" | ||||
|                               name={"custom_" + input.id} | ||||
|                               id={"custom_" + input.id} | ||||
|                               className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2" | ||||
|                                              placeholder=""/> | ||||
|                                           <label htmlFor={input.label} className="block text-sm font-medium text-gray-700">{input.label}</label> | ||||
|                                       </div>} | ||||
|                               placeholder="" | ||||
|                             /> | ||||
|                             <label htmlFor={input.label} className="block text-sm font-medium text-gray-700"> | ||||
|                               {input.label} | ||||
|                             </label> | ||||
|                           </div> | ||||
|                         )} | ||||
|                       </div> | ||||
|                     ))} | ||||
|                 <div className="mb-4"> | ||||
|                                     <label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">Additional notes</label> | ||||
|                                     <textarea name="notes" id="notes" rows={3}  className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Please share anything that will help prepare for our meeting." defaultValue={props.booking ? props.booking.description : ''}/> | ||||
|                   <label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1"> | ||||
|                     Additional notes | ||||
|                   </label> | ||||
|                   <textarea | ||||
|                     name="notes" | ||||
|                     id="notes" | ||||
|                     rows={3} | ||||
|                     className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                     placeholder="Please share anything that will help prepare for our meeting." | ||||
|                     defaultValue={props.booking ? props.booking.description : ""} | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div className="flex items-start"> | ||||
|                                     <Button type="submit" loading={loading} className="btn btn-primary">{rescheduleUid ? 'Reschedule' : 'Confirm'}</Button> | ||||
|                                     <Link href={"/" + props.user.username + "/" + props.eventType.slug + (rescheduleUid ? "?rescheduleUid=" + rescheduleUid : "")}> | ||||
|                   <Button type="submit" loading={loading} className="btn btn-primary"> | ||||
|                     {rescheduleUid ? "Reschedule" : "Confirm"} | ||||
|                   </Button> | ||||
|                   <Link | ||||
|                     href={ | ||||
|                       "/" + | ||||
|                       props.user.username + | ||||
|                       "/" + | ||||
|                       props.eventType.slug + | ||||
|                       (rescheduleUid ? "?rescheduleUid=" + rescheduleUid : "") | ||||
|                     }> | ||||
|                     <a className="ml-2 btn btn-white">Cancel</a> | ||||
|                   </Link> | ||||
|                 </div> | ||||
|               </form> | ||||
|                             {error && <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2"> | ||||
|               {error && ( | ||||
|                 <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2"> | ||||
|                   <div className="flex"> | ||||
|                     <div className="flex-shrink-0"> | ||||
|                       <ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" /> | ||||
|                     </div> | ||||
|                     <div className="ml-3"> | ||||
|                       <p className="text-sm text-yellow-700"> | ||||
|                                             Could not {rescheduleUid ? 'reschedule' : 'book'} the meeting. Please try again or{' '} | ||||
|                                             <a href={"mailto:" + props.user.email} className="font-medium underline text-yellow-700 hover:text-yellow-600"> | ||||
|                         Could not {rescheduleUid ? "reschedule" : "book"} the meeting. Please try again or{" "} | ||||
|                         <a | ||||
|                           href={"mailto:" + props.user.email} | ||||
|                           className="font-medium underline text-yellow-700 hover:text-yellow-600"> | ||||
|                           Contact {props.user.name} via e-mail | ||||
|                         </a> | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                             </div>} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </main> | ||||
|     </div> | ||||
|     ) | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getServerSideProps(context) { | ||||
|  | @ -265,11 +364,11 @@ export async function getServerSideProps(context) { | |||
|     select: { | ||||
|       username: true, | ||||
|       name: true, | ||||
|             email:true, | ||||
|       email: true, | ||||
|       bio: true, | ||||
|       avatar: true, | ||||
|             eventTypes: true | ||||
|         } | ||||
|       eventTypes: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   const eventType = await prisma.eventType.findUnique({ | ||||
|  | @ -284,25 +383,25 @@ export async function getServerSideProps(context) { | |||
|       length: true, | ||||
|       locations: true, | ||||
|       customInputs: true, | ||||
|         } | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   let booking = null; | ||||
| 
 | ||||
|     if(context.query.rescheduleUid) { | ||||
|   if (context.query.rescheduleUid) { | ||||
|     booking = await prisma.booking.findFirst({ | ||||
|       where: { | ||||
|                 uid: context.query.rescheduleUid | ||||
|         uid: context.query.rescheduleUid, | ||||
|       }, | ||||
|       select: { | ||||
|         description: true, | ||||
|         attendees: { | ||||
|           select: { | ||||
|             email: true, | ||||
|                         name: true | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             name: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | @ -310,7 +409,7 @@ export async function getServerSideProps(context) { | |||
|     props: { | ||||
|       user, | ||||
|       eventType, | ||||
|             booking | ||||
|       booking, | ||||
|     }, | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,40 +1,37 @@ | |||
| import Head from 'next/head'; | ||||
| import Link from 'next/link'; | ||||
| import { useRouter } from 'next/router'; | ||||
| import { useRef, useState, useEffect } from 'react'; | ||||
| import Select, { OptionBase } from 'react-select'; | ||||
| import prisma from '../../../lib/prisma'; | ||||
| import {LocationType} from '../../../lib/location'; | ||||
| import Shell from '../../../components/Shell'; | ||||
| import { useSession, getSession } from 'next-auth/client'; | ||||
| import { | ||||
|   LocationMarkerIcon, | ||||
|   PlusCircleIcon, | ||||
|   XIcon, | ||||
|   PhoneIcon, | ||||
| } from '@heroicons/react/outline'; | ||||
| import {EventTypeCustomInput, EventTypeCustomInputType} from "../../../lib/eventTypeInput"; | ||||
| import {PlusIcon} from "@heroicons/react/solid"; | ||||
| import Head from "next/head"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useRef, useState } from "react"; | ||||
| import Select, { OptionBase } from "react-select"; | ||||
| import prisma from "../../../lib/prisma"; | ||||
| import { LocationType } from "../../../lib/location"; | ||||
| import Shell from "../../../components/Shell"; | ||||
| import { getSession, useSession } from "next-auth/client"; | ||||
| import { LocationMarkerIcon, PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline"; | ||||
| import { EventTypeCustomInput, EventTypeCustomInputType } from "../../../lib/eventTypeInput"; | ||||
| import { PlusIcon } from "@heroicons/react/solid"; | ||||
| 
 | ||||
| export default function EventType(props) { | ||||
| export default function EventType(props: any): JSX.Element { | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const inputOptions: OptionBase[] = [ | ||||
|       { value: EventTypeCustomInputType.Text, label: 'Text' }, | ||||
|       { value: EventTypeCustomInputType.TextLong, label: 'Multiline Text' }, | ||||
|       { value: EventTypeCustomInputType.Number, label: 'Number', }, | ||||
|       { value: EventTypeCustomInputType.Bool, label: 'Checkbox', }, | ||||
|     ] | ||||
|     { value: EventTypeCustomInputType.Text, label: "Text" }, | ||||
|     { value: EventTypeCustomInputType.TextLong, label: "Multiline Text" }, | ||||
|     { value: EventTypeCustomInputType.Number, label: "Number" }, | ||||
|     { value: EventTypeCustomInputType.Bool, label: "Checkbox" }, | ||||
|   ]; | ||||
| 
 | ||||
|     const [ session, loading ] = useSession(); | ||||
|     const [ showLocationModal, setShowLocationModal ] = useState(false); | ||||
|     const [ showAddCustomModal, setShowAddCustomModal ] = useState(false); | ||||
|     const [ selectedLocation, setSelectedLocation ] = useState<OptionBase | undefined>(undefined); | ||||
|     const [ selectedInputOption, setSelectedInputOption ] = useState<OptionBase>(inputOptions[0]); | ||||
|     const [ selectedCustomInput, setSelectedCustomInput ] = useState<EventTypeCustomInput | undefined>(undefined); | ||||
|     const [ locations, setLocations ] = useState(props.eventType.locations || []); | ||||
|     const [ customInputs, setCustomInputs ] = useState<EventTypeCustomInput[]>(props.eventType.customInputs.sort((a, b) => a.id - b.id) || []); | ||||
|     const locationOptions = props.locationOptions | ||||
|   const [, loading] = useSession(); | ||||
|   const [showLocationModal, setShowLocationModal] = useState(false); | ||||
|   const [showAddCustomModal, setShowAddCustomModal] = useState(false); | ||||
|   const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined); | ||||
|   const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]); | ||||
|   const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined); | ||||
|   const [locations, setLocations] = useState(props.eventType.locations || []); | ||||
|   const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>( | ||||
|     props.eventType.customInputs.sort((a, b) => a.id - b.id) || [] | ||||
|   ); | ||||
|   const locationOptions = props.locationOptions; | ||||
| 
 | ||||
|   const titleRef = useRef<HTMLInputElement>(); | ||||
|   const slugRef = useRef<HTMLInputElement>(); | ||||
|  | @ -58,35 +55,45 @@ export default function EventType(props) { | |||
|     const enteredEventName = eventNameRef.current.value; | ||||
|     // TODO: Add validation
 | ||||
| 
 | ||||
|         const response = await fetch('/api/availability/eventtype', { | ||||
|             method: 'PATCH', | ||||
|             body: JSON.stringify({id: props.eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden, locations, eventName: enteredEventName, customInputs }), | ||||
|     await fetch("/api/availability/eventtype", { | ||||
|       method: "PATCH", | ||||
|       body: JSON.stringify({ | ||||
|         id: props.eventType.id, | ||||
|         title: enteredTitle, | ||||
|         slug: enteredSlug, | ||||
|         description: enteredDescription, | ||||
|         length: enteredLength, | ||||
|         hidden: enteredIsHidden, | ||||
|         locations, | ||||
|         eventName: enteredEventName, | ||||
|         customInputs, | ||||
|       }), | ||||
|       headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             } | ||||
|         "Content-Type": "application/json", | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|         router.push('/availability'); | ||||
|     router.push("/availability"); | ||||
|   } | ||||
| 
 | ||||
|   async function deleteEventTypeHandler(event) { | ||||
|     event.preventDefault(); | ||||
| 
 | ||||
|         const response = await fetch('/api/availability/eventtype', { | ||||
|             method: 'DELETE', | ||||
|             body: JSON.stringify({id: props.eventType.id}), | ||||
|     await fetch("/api/availability/eventtype", { | ||||
|       method: "DELETE", | ||||
|       body: JSON.stringify({ id: props.eventType.id }), | ||||
|       headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             } | ||||
|         "Content-Type": "application/json", | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|         router.push('/availability'); | ||||
|     router.push("/availability"); | ||||
|   } | ||||
| 
 | ||||
|   const openLocationModal = (type: LocationType) => { | ||||
|         setSelectedLocation(locationOptions.find( (option) => option.value === type)); | ||||
|     setSelectedLocation(locationOptions.find((option) => option.value === type)); | ||||
|     setShowLocationModal(true); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const closeLocationModal = () => { | ||||
|     setSelectedLocation(undefined); | ||||
|  | @ -101,9 +108,9 @@ export default function EventType(props) { | |||
| 
 | ||||
|   const openEditCustomModel = (customInput: EventTypeCustomInput) => { | ||||
|     setSelectedCustomInput(customInput); | ||||
|       setSelectedInputOption(inputOptions.find(e => e.value === customInput.type)); | ||||
|     setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)); | ||||
|     setShowAddCustomModal(true); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const LocationOptions = () => { | ||||
|     if (!selectedLocation) { | ||||
|  | @ -111,26 +118,31 @@ export default function EventType(props) { | |||
|     } | ||||
|     switch (selectedLocation.value) { | ||||
|       case LocationType.InPerson: | ||||
|                 const address = locations.find( | ||||
|                     (location) => location.type === LocationType.InPerson | ||||
|                 )?.address; | ||||
|         return ( | ||||
|           <div> | ||||
|                         <label htmlFor="address" className="block text-sm font-medium text-gray-700">Set an address or place</label> | ||||
|             <label htmlFor="address" className="block text-sm font-medium text-gray-700"> | ||||
|               Set an address or place | ||||
|             </label> | ||||
|             <div className="mt-1"> | ||||
|                             <input type="text" name="address" id="address" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" defaultValue={address} /> | ||||
|               <input | ||||
|                 type="text" | ||||
|                 name="address" | ||||
|                 id="address" | ||||
|                 required | ||||
|                 className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                 defaultValue={locations.find((location) => location.type === LocationType.InPerson)?.address} | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|                 ) | ||||
|         ); | ||||
|       case LocationType.Phone: | ||||
| 
 | ||||
|         return ( | ||||
|           <p className="text-sm">Calendso will ask your invitee to enter a phone number before scheduling.</p> | ||||
|                 ) | ||||
|         ); | ||||
|       case LocationType.GoogleMeet: | ||||
|                  return ( | ||||
|                     <p className="text-sm">Calendso will provide a Google Meet location.</p> | ||||
|                 ) | ||||
|         return <p className="text-sm">Calendso will provide a Google Meet location.</p>; | ||||
|       case LocationType.Zoom: | ||||
|         return <p className="text-sm">Calendso will provide a Zoom meeting URL.</p>; | ||||
|     } | ||||
|     return null; | ||||
|   }; | ||||
|  | @ -143,10 +155,10 @@ export default function EventType(props) { | |||
|       details = { address: e.target.address.value }; | ||||
|     } | ||||
| 
 | ||||
|         const existingIdx = locations.findIndex( (loc) => e.target.location.value === loc.type ); | ||||
|     const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type); | ||||
|     if (existingIdx !== -1) { | ||||
|             let copy = locations; | ||||
|             copy[ existingIdx ] = { ...locations[ existingIdx ], ...details }; | ||||
|       const copy = locations; | ||||
|       copy[existingIdx] = { ...locations[existingIdx], ...details }; | ||||
|       setLocations(copy); | ||||
|     } else { | ||||
|       setLocations(locations.concat({ type: e.target.location.value, ...details })); | ||||
|  | @ -156,7 +168,7 @@ export default function EventType(props) { | |||
|   }; | ||||
| 
 | ||||
|   const removeLocation = (selectedLocation) => { | ||||
|         setLocations(locations.filter( (location) => location.type !== selectedLocation.type )); | ||||
|     setLocations(locations.filter((location) => location.type !== selectedLocation.type)); | ||||
|   }; | ||||
| 
 | ||||
|   const updateCustom = (e) => { | ||||
|  | @ -165,11 +177,11 @@ export default function EventType(props) { | |||
|     const customInput: EventTypeCustomInput = { | ||||
|       label: e.target.label.value, | ||||
|       required: e.target.required.checked, | ||||
|         type: e.target.type.value | ||||
|       type: e.target.type.value, | ||||
|     }; | ||||
| 
 | ||||
|       if (!!e.target.id?.value) { | ||||
|         const index = customInputs.findIndex(inp => inp.id === +e.target.id?.value); | ||||
|     if (e.target.id?.value) { | ||||
|       const index = customInputs.findIndex((inp) => inp.id === +e.target.id?.value); | ||||
|       if (index >= 0) { | ||||
|         const input = customInputs[index]; | ||||
|         input.label = customInput.label; | ||||
|  | @ -177,7 +189,7 @@ export default function EventType(props) { | |||
|         input.type = customInput.type; | ||||
|         setCustomInputs(customInputs); | ||||
|       } | ||||
|       } else{ | ||||
|     } else { | ||||
|       setCustomInputs(customInputs.concat(customInput)); | ||||
|     } | ||||
|     closeAddCustomModal(); | ||||
|  | @ -185,12 +197,12 @@ export default function EventType(props) { | |||
| 
 | ||||
|   const removeCustom = (customInput, e) => { | ||||
|     e.preventDefault(); | ||||
|       const index = customInputs.findIndex(inp => inp.id === customInput.id); | ||||
|       if (index >= 0){ | ||||
|     const index = customInputs.findIndex((inp) => inp.id === customInput.id); | ||||
|     if (index >= 0) { | ||||
|       customInputs.splice(index, 1); | ||||
|       setCustomInputs([...customInputs]); | ||||
|     } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|  | @ -198,20 +210,33 @@ export default function EventType(props) { | |||
|         <title>{props.eventType.title} | Event Type | Calendso</title> | ||||
|         <link rel="icon" href="/favicon.ico" /> | ||||
|       </Head> | ||||
|         <Shell heading={'Event Type - ' + props.eventType.title}> | ||||
|       <Shell heading={"Event Type - " + props.eventType.title}> | ||||
|         <div> | ||||
|           <div className="mb-8"> | ||||
|             <div className="bg-white overflow-hidden shadow rounded-lg"> | ||||
|               <div className="px-4 py-5 sm:p-6"> | ||||
|                 <form onSubmit={updateEventTypeHandler}> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label> | ||||
|                     <label htmlFor="title" className="block text-sm font-medium text-gray-700"> | ||||
|                       Title | ||||
|                     </label> | ||||
|                     <div className="mt-1"> | ||||
|                         <input ref={titleRef} type="text" name="title" id="title" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Quick Chat" defaultValue={props.eventType.title} /> | ||||
|                       <input | ||||
|                         ref={titleRef} | ||||
|                         type="text" | ||||
|                         name="title" | ||||
|                         id="title" | ||||
|                         required | ||||
|                         className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                         placeholder="Quick Chat" | ||||
|                         defaultValue={props.eventType.title} | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="slug" className="block text-sm font-medium text-gray-700">URL</label> | ||||
|                     <label htmlFor="slug" className="block text-sm font-medium text-gray-700"> | ||||
|                       URL | ||||
|                     </label> | ||||
|                     <div className="mt-1"> | ||||
|                       <div className="flex rounded-md shadow-sm"> | ||||
|                         <span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm"> | ||||
|  | @ -230,8 +255,11 @@ export default function EventType(props) { | |||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="location" className="block text-sm font-medium text-gray-700">Location</label> | ||||
|                       {locations.length === 0 && <div className="mt-1 mb-2"> | ||||
|                     <label htmlFor="location" className="block text-sm font-medium text-gray-700"> | ||||
|                       Location | ||||
|                     </label> | ||||
|                     {locations.length === 0 && ( | ||||
|                       <div className="mt-1 mb-2"> | ||||
|                         <div className="flex rounded-md shadow-sm"> | ||||
|                           <Select | ||||
|                             name="location" | ||||
|  | @ -242,9 +270,11 @@ export default function EventType(props) { | |||
|                             onChange={(e) => openLocationModal(e.value)} | ||||
|                           /> | ||||
|                         </div> | ||||
|                       </div>} | ||||
|                       {locations.length > 0 && <ul className="w-96 mt-1"> | ||||
|                         {locations.map( (location) => ( | ||||
|                       </div> | ||||
|                     )} | ||||
|                     {locations.length > 0 && ( | ||||
|                       <ul className="w-96 mt-1"> | ||||
|                         {locations.map((location) => ( | ||||
|                           <li key={location.type} className="bg-blue-50 mb-2 p-2 border"> | ||||
|                             <div className="flex justify-between"> | ||||
|                               {location.type === LocationType.InPerson && ( | ||||
|  | @ -261,12 +291,73 @@ export default function EventType(props) { | |||
|                               )} | ||||
|                               {location.type === LocationType.GoogleMeet && ( | ||||
|                                 <div className="flex-grow flex"> | ||||
|                                   <svg className="h-6 w-6" stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><title></title><path d="M12 0C6.28 0 1.636 4.641 1.636 10.364c0 5.421 4.945 9.817 10.364 9.817V24c6.295-3.194 10.364-8.333 10.364-13.636C22.364 4.64 17.72 0 12 0zM7.5 6.272h6.817a1.363 1.363 0 0 1 1.365 1.365v1.704l2.728-2.727v7.501l-2.726-2.726v1.703a1.362 1.362 0 0 1-1.365 1.365H7.5c-.35 0-.698-.133-.965-.4a1.358 1.358 0 0 1-.4-.965V7.637A1.362 1.362 0 0 1 7.5 6.272Z"></path></svg> | ||||
|                                   <svg | ||||
|                                     className="h-6 w-6" | ||||
|                                     stroke="currentColor" | ||||
|                                     fill="currentColor" | ||||
|                                     strokeWidth="0" | ||||
|                                     role="img" | ||||
|                                     viewBox="0 0 24 24" | ||||
|                                     height="1em" | ||||
|                                     width="1em" | ||||
|                                     xmlns="http://www.w3.org/2000/svg"> | ||||
|                                     <title></title> | ||||
|                                     <path d="M12 0C6.28 0 1.636 4.641 1.636 10.364c0 5.421 4.945 9.817 10.364 9.817V24c6.295-3.194 10.364-8.333 10.364-13.636C22.364 4.64 17.72 0 12 0zM7.5 6.272h6.817a1.363 1.363 0 0 1 1.365 1.365v1.704l2.728-2.727v7.501l-2.726-2.726v1.703a1.362 1.362 0 0 1-1.365 1.365H7.5c-.35 0-.698-.133-.965-.4a1.358 1.358 0 0 1-.4-.965V7.637A1.362 1.362 0 0 1 7.5 6.272Z"></path> | ||||
|                                   </svg> | ||||
|                                   <span className="ml-2 text-sm">Google Meet</span> | ||||
|                                 </div> | ||||
|                               )} | ||||
|                               {location.type === LocationType.Zoom && ( | ||||
|                                 <div className="flex-grow flex"> | ||||
|                                   <svg | ||||
|                                     xmlns="http://www.w3.org/2000/svg" | ||||
|                                     viewBox="0 0 1329.08 1329.08" | ||||
|                                     height="1.25em" | ||||
|                                     width="1.25em" | ||||
|                                     shapeRendering="geometricPrecision" | ||||
|                                     textRendering="geometricPrecision" | ||||
|                                     imageRendering="optimizeQuality" | ||||
|                                     fillRule="evenodd" | ||||
|                                     clipRule="evenodd"> | ||||
|                                     <g id="Layer_x0020_1"> | ||||
|                                       <g id="_2116467169744"> | ||||
|                                         <path | ||||
|                                           d="M664.54 0c367.02 0 664.54 297.52 664.54 664.54s-297.52 664.54-664.54 664.54S0 1031.56 0 664.54 297.52 0 664.54 0z" | ||||
|                                           fill="#e5e5e4" | ||||
|                                           fillRule="nonzero" | ||||
|                                         /> | ||||
|                                         <path | ||||
|                                           style={{ | ||||
|                                             fill: "#fff", | ||||
|                                             fillRule: "nonzero", | ||||
|                                           }} | ||||
|                                           d="M664.54 12.94c359.87 0 651.6 291.73 651.6 651.6s-291.73 651.6-651.6 651.6-651.6-291.73-651.6-651.6 291.74-651.6 651.6-651.6z" | ||||
|                                         /> | ||||
|                                         <path | ||||
|                                           d="M664.54 65.21c331 0 599.33 268.33 599.33 599.33 0 331-268.33 599.33-599.33 599.33-331 0-599.33-268.33-599.33-599.33 0-331 268.33-599.33 599.33-599.33z" | ||||
|                                           fill="#4a8cff" | ||||
|                                           fillRule="nonzero" | ||||
|                                         /> | ||||
|                                         <path | ||||
|                                           style={{ | ||||
|                                             fill: "#fff", | ||||
|                                             fillRule: "nonzero", | ||||
|                                           }} | ||||
|                                           d="M273.53 476.77v281.65c.25 63.69 52.27 114.95 115.71 114.69h410.55c11.67 0 21.06-9.39 21.06-20.81V570.65c-.25-63.69-52.27-114.95-115.7-114.69H294.6c-11.67 0-21.06 9.39-21.06 20.81zm573.45 109.87l169.5-123.82c14.72-12.18 26.13-9.14 26.13 12.94v377.56c0 25.12-13.96 22.08-26.13 12.94l-169.5-123.57V586.64z" | ||||
|                                         /> | ||||
|                                       </g> | ||||
|                                     </g> | ||||
|                                   </svg> | ||||
|                                   <span className="ml-2 text-sm">Zoom Video</span> | ||||
|                                 </div> | ||||
|                               )} | ||||
|                               <div className="flex"> | ||||
|                                 <button type="button" onClick={() => openLocationModal(location.type)} className="mr-2 text-sm text-blue-600">Edit</button> | ||||
|                                 <button | ||||
|                                   type="button" | ||||
|                                   onClick={() => openLocationModal(location.type)} | ||||
|                                   className="mr-2 text-sm text-blue-600"> | ||||
|                                   Edit | ||||
|                                 </button> | ||||
|                                 <button onClick={() => removeLocation(location)}> | ||||
|                                   <XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " /> | ||||
|                                 </button> | ||||
|  | @ -274,39 +365,76 @@ export default function EventType(props) { | |||
|                             </div> | ||||
|                           </li> | ||||
|                         ))} | ||||
|                         {locations.length > 0 && locations.length !== locationOptions.length && <li> | ||||
|                           <button type="button" className="sm:flex sm:items-start text-sm text-blue-600" onClick={() => setShowLocationModal(true)}> | ||||
|                         {locations.length > 0 && locations.length !== locationOptions.length && ( | ||||
|                           <li> | ||||
|                             <button | ||||
|                               type="button" | ||||
|                               className="sm:flex sm:items-start text-sm text-blue-600" | ||||
|                               onClick={() => setShowLocationModal(true)}> | ||||
|                               <PlusCircleIcon className="h-6 w-6" /> | ||||
|                               <span className="ml-1">Add another location option</span> | ||||
|                             </button> | ||||
|                         </li>} | ||||
|                       </ul>} | ||||
|                           </li> | ||||
|                         )} | ||||
|                       </ul> | ||||
|                     )} | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label> | ||||
|                     <label htmlFor="description" className="block text-sm font-medium text-gray-700"> | ||||
|                       Description | ||||
|                     </label> | ||||
|                     <div className="mt-1"> | ||||
|                         <textarea ref={descriptionRef} name="description" id="description" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="A quick video meeting." defaultValue={props.eventType.description}></textarea> | ||||
|                       <textarea | ||||
|                         ref={descriptionRef} | ||||
|                         name="description" | ||||
|                         id="description" | ||||
|                         className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                         placeholder="A quick video meeting." | ||||
|                         defaultValue={props.eventType.description}></textarea> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="length" className="block text-sm font-medium text-gray-700">Length</label> | ||||
|                     <label htmlFor="length" className="block text-sm font-medium text-gray-700"> | ||||
|                       Length | ||||
|                     </label> | ||||
|                     <div className="mt-1 relative rounded-md shadow-sm"> | ||||
|                         <input ref={lengthRef} type="number" name="length" id="length" required className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md" placeholder="15" defaultValue={props.eventType.length} /> | ||||
|                       <input | ||||
|                         ref={lengthRef} | ||||
|                         type="number" | ||||
|                         name="length" | ||||
|                         id="length" | ||||
|                         required | ||||
|                         className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md" | ||||
|                         placeholder="15" | ||||
|                         defaultValue={props.eventType.length} | ||||
|                       /> | ||||
|                       <div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm"> | ||||
|                         minutes | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="eventName" className="block text-sm font-medium text-gray-700">Calendar entry name</label> | ||||
|                     <label htmlFor="eventName" className="block text-sm font-medium text-gray-700"> | ||||
|                       Calendar entry name | ||||
|                     </label> | ||||
|                     <div className="mt-1 relative rounded-md shadow-sm"> | ||||
|                         <input ref={eventNameRef} type="text" name="title" id="title" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Meeting with {USER}" defaultValue={props.eventType.eventName} /> | ||||
|                       <input | ||||
|                         ref={eventNameRef} | ||||
|                         type="text" | ||||
|                         name="title" | ||||
|                         id="title" | ||||
|                         className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                         placeholder="Meeting with {USER}" | ||||
|                         defaultValue={props.eventType.eventName} | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="mb-4"> | ||||
|                       <label htmlFor="additionalFields" className="block text-sm font-medium text-gray-700">Additional Inputs</label> | ||||
|                     <label htmlFor="additionalFields" className="block text-sm font-medium text-gray-700"> | ||||
|                       Additional Inputs | ||||
|                     </label> | ||||
|                     <ul className="w-96 mt-1"> | ||||
|                         {customInputs.map( (customInput) => ( | ||||
|                       {customInputs.map((customInput) => ( | ||||
|                         <li key={customInput.label} className="bg-blue-50 mb-2 p-2 border"> | ||||
|                           <div className="flex justify-between"> | ||||
|                             <div> | ||||
|  | @ -317,22 +445,30 @@ export default function EventType(props) { | |||
|                                 <span className="ml-2 text-sm">Type: {customInput.type}</span> | ||||
|                               </div> | ||||
|                               <div> | ||||
|                                   <span | ||||
|                                     className="ml-2 text-sm">{customInput.required ? "Required" : "Optional"}</span> | ||||
|                                 <span className="ml-2 text-sm"> | ||||
|                                   {customInput.required ? "Required" : "Optional"} | ||||
|                                 </span> | ||||
|                               </div> | ||||
|                             </div> | ||||
|                             <div className="flex"> | ||||
|                                 <button type="button" onClick={() => openEditCustomModel(customInput)} className="mr-2 text-sm text-blue-600">Edit | ||||
|                               <button | ||||
|                                 type="button" | ||||
|                                 onClick={() => openEditCustomModel(customInput)} | ||||
|                                 className="mr-2 text-sm text-blue-600"> | ||||
|                                 Edit | ||||
|                               </button> | ||||
|                               <button onClick={(e) => removeCustom(customInput, e)}> | ||||
|                                   <XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 "/> | ||||
|                                 <XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " /> | ||||
|                               </button> | ||||
|                             </div> | ||||
|                           </div> | ||||
|                         </li> | ||||
|                       ))} | ||||
|                       <li> | ||||
|                             <button type="button" className="sm:flex sm:items-start text-sm text-blue-600" onClick={() => setShowAddCustomModal(true)}> | ||||
|                         <button | ||||
|                           type="button" | ||||
|                           className="sm:flex sm:items-start text-sm text-blue-600" | ||||
|                           onClick={() => setShowAddCustomModal(true)}> | ||||
|                           <PlusCircleIcon className="h-6 w-6" /> | ||||
|                           <span className="ml-1">Add another input</span> | ||||
|                         </button> | ||||
|  | @ -355,12 +491,18 @@ export default function EventType(props) { | |||
|                         <label htmlFor="ishidden" className="font-medium text-gray-700"> | ||||
|                           Hide this event type | ||||
|                         </label> | ||||
|                           <p className="text-gray-500">Hide the event type from your page, so it can only be booked through it's URL.</p> | ||||
|                         <p className="text-gray-500"> | ||||
|                           Hide the event type from your page, so it can only be booked through its URL. | ||||
|                         </p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                     <button type="submit" className="btn btn-primary">Update</button> | ||||
|                     <Link href="/availability"><a className="ml-2 btn btn-white">Cancel</a></Link> | ||||
|                   <button type="submit" className="btn btn-primary"> | ||||
|                     Update | ||||
|                   </button> | ||||
|                   <Link href="/availability"> | ||||
|                     <a className="ml-2 btn btn-white">Cancel</a> | ||||
|                   </Link> | ||||
|                 </form> | ||||
|               </div> | ||||
|             </div> | ||||
|  | @ -368,16 +510,15 @@ export default function EventType(props) { | |||
|           <div> | ||||
|             <div className="bg-white shadow sm:rounded-lg"> | ||||
|               <div className="px-4 py-5 sm:p-6"> | ||||
|                   <h3 className="text-lg mb-2 leading-6 font-medium text-gray-900"> | ||||
|                     Delete this event type | ||||
|                   </h3> | ||||
|                 <h3 className="text-lg mb-2 leading-6 font-medium text-gray-900">Delete this event type</h3> | ||||
|                 <div className="mb-4 max-w-xl text-sm text-gray-500"> | ||||
|                     <p> | ||||
|                       Once you delete this event type, it will be permanently removed. | ||||
|                     </p> | ||||
|                   <p>Once you delete this event type, it will be permanently removed.</p> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <button onClick={deleteEventTypeHandler} type="button" className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"> | ||||
|                   <button | ||||
|                     onClick={deleteEventTypeHandler} | ||||
|                     type="button" | ||||
|                     className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"> | ||||
|                     Delete event type | ||||
|                   </button> | ||||
|                 </div> | ||||
|  | @ -385,12 +526,20 @@ export default function EventType(props) { | |||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|           {showLocationModal && | ||||
|           <div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> | ||||
|         {showLocationModal && ( | ||||
|           <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"> | ||||
|               <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> | ||||
|               <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> | ||||
|               <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> | ||||
|                 ​ | ||||
|               </span> | ||||
| 
 | ||||
|               <div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> | ||||
|                 <div className="sm:flex sm:items-start mb-4"> | ||||
|  | @ -398,7 +547,9 @@ export default function EventType(props) { | |||
|                     <LocationMarkerIcon 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">Edit location</h3> | ||||
|                     <h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> | ||||
|                       Edit location | ||||
|                     </h3> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <form onSubmit={updateLocations}> | ||||
|  | @ -423,13 +574,22 @@ export default function EventType(props) { | |||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           } | ||||
|           {showAddCustomModal && | ||||
|           <div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> | ||||
|         )} | ||||
|         {showAddCustomModal && ( | ||||
|           <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"> | ||||
|                   <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"/> | ||||
|               <div | ||||
|                 className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" | ||||
|                 aria-hidden="true" | ||||
|               /> | ||||
| 
 | ||||
|                   <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | ||||
|               <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> | ||||
|                 ​ | ||||
|               </span> | ||||
| 
 | ||||
|               <div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> | ||||
|                 <div className="sm:flex sm:items-start mb-4"> | ||||
|  | @ -437,7 +597,9 @@ export default function EventType(props) { | |||
|                     <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 new custom input field</h3> | ||||
|                     <h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title"> | ||||
|                       Add new custom input field | ||||
|                     </h3> | ||||
|                     <div> | ||||
|                       <p className="text-sm text-gray-400"> | ||||
|                         This input will be shown when booking this event | ||||
|  | @ -447,7 +609,9 @@ export default function EventType(props) { | |||
|                 </div> | ||||
|                 <form onSubmit={updateCustom}> | ||||
|                   <div className="mb-2"> | ||||
|                             <label htmlFor="type" className="block text-sm font-medium text-gray-700">Input type</label> | ||||
|                     <label htmlFor="type" className="block text-sm font-medium text-gray-700"> | ||||
|                       Input type | ||||
|                     </label> | ||||
|                     <Select | ||||
|                       name="type" | ||||
|                       defaultValue={selectedInputOption} | ||||
|  | @ -459,21 +623,34 @@ export default function EventType(props) { | |||
|                     /> | ||||
|                   </div> | ||||
|                   <div className="mb-2"> | ||||
|                               <label htmlFor="label" className="block text-sm font-medium text-gray-700">Label</label> | ||||
|                     <label htmlFor="label" className="block text-sm font-medium text-gray-700"> | ||||
|                       Label | ||||
|                     </label> | ||||
|                     <div className="mt-1"> | ||||
|                                   <input type="text" name="label" id="label" required | ||||
|                       <input | ||||
|                         type="text" | ||||
|                         name="label" | ||||
|                         id="label" | ||||
|                         required | ||||
|                         className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" | ||||
|                                          defaultValue={selectedCustomInput?.label}/> | ||||
|                         defaultValue={selectedCustomInput?.label} | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div className="flex items-center h-5"> | ||||
|                               <input id="required" name="required" type="checkbox" className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2" defaultChecked={selectedCustomInput?.required ?? true}/> | ||||
|                     <input | ||||
|                       id="required" | ||||
|                       name="required" | ||||
|                       type="checkbox" | ||||
|                       className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2" | ||||
|                       defaultChecked={selectedCustomInput?.required ?? true} | ||||
|                     /> | ||||
|                     <label htmlFor="required" className="block text-sm font-medium text-gray-700"> | ||||
|                       Is required | ||||
|                     </label> | ||||
|                   </div> | ||||
| 
 | ||||
|                           <input type="hidden" name="id" id="id" value={selectedCustomInput?.id}/> | ||||
|                   <input type="hidden" name="id" id="id" value={selectedCustomInput?.id} /> | ||||
| 
 | ||||
|                   <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> | ||||
|                     <button type="submit" className="btn btn-primary"> | ||||
|  | @ -487,7 +664,7 @@ export default function EventType(props) { | |||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           } | ||||
|         )} | ||||
|       </Shell> | ||||
|     </div> | ||||
|   ); | ||||
|  | @ -499,23 +676,24 @@ const validJson = (jsonString: string) => { | |||
|     if (o && typeof o === "object") { | ||||
|       return o; | ||||
|     } | ||||
|   } catch (e) { | ||||
|     console.log("Invalid JSON:", e); | ||||
|   } | ||||
|   catch (e) {} | ||||
|   return false; | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| export async function getServerSideProps(context) { | ||||
|   const session = await getSession(context); | ||||
|   if (!session) { | ||||
|         return { redirect: { permanent: false, destination: '/auth/login' } }; | ||||
|     return { redirect: { permanent: false, destination: "/auth/login" } }; | ||||
|   } | ||||
|   const user = await prisma.user.findFirst({ | ||||
|     where: { | ||||
|       email: session.user.email, | ||||
|     }, | ||||
|     select: { | ||||
|             username: true | ||||
|         } | ||||
|       username: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   const credentials = await prisma.credential.findMany({ | ||||
|  | @ -525,37 +703,45 @@ export async function getServerSideProps(context) { | |||
|     select: { | ||||
|       id: true, | ||||
|       type: true, | ||||
|             key: true | ||||
|         } | ||||
|       key: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|     const integrations = [ { | ||||
|   const integrations = [ | ||||
|     { | ||||
|       installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), | ||||
|         enabled: credentials.find( (integration) => integration.type === "google_calendar" ) != null, | ||||
|       enabled: 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", | ||||
|         enabled: credentials.find( (integration) => integration.type === "office365_calendar" ) != null, | ||||
|       enabled: 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", | ||||
|     } ]; | ||||
| 
 | ||||
|     let locationOptions: OptionBase[] = [ | ||||
|         { value: LocationType.InPerson, label: 'In-person meeting' }, | ||||
|         { value: LocationType.Phone, label: 'Phone call', }, | ||||
|     }, | ||||
|   ]; | ||||
| 
 | ||||
|       const hasGoogleCalendarIntegration = integrations.find((i) => i.type === "google_calendar" && i.installed === true && i.enabled) | ||||
|   const locationOptions: OptionBase[] = [ | ||||
|     { value: LocationType.InPerson, label: "In-person meeting" }, | ||||
|     { value: LocationType.Phone, label: "Phone call" }, | ||||
|     { value: LocationType.Zoom, label: "Zoom Video" }, | ||||
|   ]; | ||||
| 
 | ||||
|   const hasGoogleCalendarIntegration = integrations.find( | ||||
|     (i) => i.type === "google_calendar" && i.installed === true && i.enabled | ||||
|   ); | ||||
|   if (hasGoogleCalendarIntegration) { | ||||
|         locationOptions.push( { value: LocationType.GoogleMeet, label: 'Google Meet' }) | ||||
|     locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" }); | ||||
|   } | ||||
| 
 | ||||
|       const hasOfficeIntegration = integrations.find((i) => i.type === "office365_calendar" && i.installed === true && i.enabled) | ||||
|   const hasOfficeIntegration = integrations.find( | ||||
|     (i) => i.type === "office365_calendar" && i.installed === true && i.enabled | ||||
|   ); | ||||
|   if (hasOfficeIntegration) { | ||||
|     // TODO: Add default meeting option of the office integration.
 | ||||
|     // Assuming it's Microsoft Teams.
 | ||||
|  | @ -574,15 +760,15 @@ export async function getServerSideProps(context) { | |||
|       hidden: true, | ||||
|       locations: true, | ||||
|       eventName: true, | ||||
|             customInputs: true | ||||
|         } | ||||
|       customInputs: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|       user, | ||||
|       eventType, | ||||
|             locationOptions | ||||
|       locationOptions, | ||||
|     }, | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas