import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import Modal from "../../components/Modal"; import React, { useEffect, useRef, useState } from "react"; import Select, { OptionBase } from "react-select"; import prisma from "@lib/prisma"; import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; import { LocationType } from "@lib/location"; import Shell from "@components/Shell"; import { getSession } from "next-auth/client"; import { Scheduler } from "@components/ui/Scheduler"; import { Disclosure, RadioGroup } from "@headlessui/react"; import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { LocationMarkerIcon, LinkIcon, PlusIcon, DocumentIcon, ChevronRightIcon, ClockIcon, TrashIcon, ExternalLinkIcon, } from "@heroicons/react/solid"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import { Availability, EventType, User } from "@prisma/client"; import { validJson } from "@lib/jsonUtils"; import classnames from "classnames"; import throttle from "lodash.throttle"; import "react-dates/initialize"; import "react-dates/lib/css/_datepicker.css"; import { DateRangePicker, OrientationShape, toMomentObject } from "react-dates"; import Switch from "@components/ui/Switch"; import { Dialog, DialogTrigger } from "@components/Dialog"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useMutation } from "react-query"; import { EventTypeInput } from "@lib/types/event-type"; import updateEventType from "@lib/mutations/event-types/update-event-type"; import deleteEventType from "@lib/mutations/event-types/delete-event-type"; import showToast from "@lib/notification"; dayjs.extend(utc); dayjs.extend(timezone); const PERIOD_TYPES = [ { type: "rolling", suffix: "into the future", }, { type: "range", prefix: "Within a date range", }, { type: "unlimited", prefix: "Indefinitely into the future", }, ]; const EventTypePage = (props: InferGetServerSidePropsType) => { const { user, eventType, locationOptions, availability } = props; const router = useRouter(); const [successModalOpen, setSuccessModalOpen] = useState(false); const inputOptions: OptionBase[] = [ { value: EventTypeCustomInputType.TEXT, label: "Text" }, { value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" }, { value: EventTypeCustomInputType.NUMBER, label: "Number" }, { value: EventTypeCustomInputType.BOOL, label: "Checkbox" }, ]; const [DATE_PICKER_ORIENTATION, setDatePickerOrientation] = useState("horizontal"); const [contentSize, setContentSize] = useState({ width: 0, height: 0 }); const updateMutation = useMutation(updateEventType, { onSuccess: async ({ eventType }) => { await router.push("/event-types"); showToast(`${eventType.title} event type updated successfully`, "success"); }, onError: (err: Error) => { showToast(err.message, "error"); }, }); const deleteMutation = useMutation(deleteEventType, { onSuccess: async () => { await router.push("/event-types"); showToast("Event type deleted successfully", "success"); }, onError: (err: Error) => { showToast(err.message, "error"); }, }); const handleResizeEvent = () => { const elementWidth = parseFloat(getComputedStyle(document.body).width); const elementHeight = parseFloat(getComputedStyle(document.body).height); setContentSize({ width: elementWidth, height: elementHeight, }); }; const throttledHandleResizeEvent = throttle(handleResizeEvent, 100); useEffect(() => { handleResizeEvent(); window.addEventListener("resize", throttledHandleResizeEvent); return () => { window.removeEventListener("resize", throttledHandleResizeEvent); }; }, []); useEffect(() => { if (contentSize.width < 500) { setDatePickerOrientation("vertical"); } else { setDatePickerOrientation("horizontal"); } }, [contentSize]); const [enteredAvailability, setEnteredAvailability] = useState(); const [showLocationModal, setShowLocationModal] = useState(false); const [showAddCustomModal, setShowAddCustomModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); const [selectedInputOption, setSelectedInputOption] = useState(inputOptions[0]); const [locations, setLocations] = useState(eventType.locations || []); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); const [periodStartDate, setPeriodStartDate] = useState(() => { if (eventType.periodType === "range" && eventType?.periodStartDate) { return toMomentObject(new Date(eventType.periodStartDate)); } return null; }); const [periodEndDate, setPeriodEndDate] = useState(() => { if (eventType.periodType === "range" && eventType.periodEndDate) { return toMomentObject(new Date(eventType?.periodEndDate)); } return null; }); const [focusedInput, setFocusedInput] = useState(null); const [periodType, setPeriodType] = useState(() => { return ( PERIOD_TYPES.find((s) => s.type === eventType.periodType) || PERIOD_TYPES.find((s) => s.type === "unlimited") ); }); const [hidden, setHidden] = useState(eventType.hidden); const titleRef = useRef(); const slugRef = useRef(); const descriptionRef = useRef(); const lengthRef = useRef(); const requiresConfirmationRef = useRef(); const eventNameRef = useRef(); const periodDaysRef = useRef(); const periodDaysTypeRef = useRef(); useEffect(() => { setSelectedTimeZone(eventType.timeZone || user.timeZone); }, []); async function updateEventTypeHandler(event) { event.preventDefault(); const enteredTitle: string = titleRef.current.value; const enteredSlug: string = slugRef.current.value; const enteredDescription: string = descriptionRef.current.value; const enteredLength: number = parseInt(lengthRef.current.value); const advancedOptionsPayload: AdvancedOptions = {}; if (requiresConfirmationRef.current) { advancedOptionsPayload.requiresConfirmation = requiresConfirmationRef.current.checked; advancedOptionsPayload.eventName = eventNameRef.current.value; advancedOptionsPayload.periodType = periodType.type; advancedOptionsPayload.periodDays = parseInt(periodDaysRef?.current?.value); advancedOptionsPayload.periodCountCalendarDays = Boolean(parseInt(periodDaysTypeRef?.current.value)); advancedOptionsPayload.periodStartDate = periodStartDate ? periodStartDate.toDate() : null; advancedOptionsPayload.periodEndDate = periodEndDate ? periodEndDate.toDate() : null; } const payload: EventTypeInput = { id: eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden, locations, customInputs, timeZone: selectedTimeZone, availability: enteredAvailability || null, ...advancedOptionsPayload, }; updateMutation.mutate(payload); } async function deleteEventTypeHandler(event) { event.preventDefault(); const payload = { id: eventType.id }; deleteMutation.mutate(payload); } const openLocationModal = (type: LocationType) => { setSelectedLocation(locationOptions.find((option) => option.value === type)); setShowLocationModal(true); }; const closeLocationModal = () => { setSelectedLocation(undefined); setShowLocationModal(false); }; const closeAddCustomModal = () => { setSelectedInputOption(inputOptions[0]); setShowAddCustomModal(false); setSelectedCustomInput(undefined); }; const closeSuccessModal = () => { setSuccessModalOpen(false); }; const updateLocations = (e) => { e.preventDefault(); let details = {}; if (e.target.location.value === LocationType.InPerson) { details = { address: e.target.address.value }; } const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type); if (existingIdx !== -1) { const copy = locations; copy[existingIdx] = { ...locations[existingIdx], ...details }; setLocations(copy); } else { setLocations(locations.concat({ type: e.target.location.value, ...details })); } setShowLocationModal(false); }; const removeLocation = (selectedLocation) => { setLocations(locations.filter((location) => location.type !== selectedLocation.type)); }; const openEditCustomModel = (customInput: EventTypeCustomInput) => { setSelectedCustomInput(customInput); setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)); setShowAddCustomModal(true); }; const LocationOptions = () => { if (!selectedLocation) { return null; } switch (selectedLocation.value) { case LocationType.InPerson: return (
location.type === LocationType.InPerson)?.address} />
); case LocationType.Phone: return (

Calendso will ask your invitee to enter a phone number before scheduling.

); case LocationType.GoogleMeet: return

Calendso will provide a Google Meet location.

; case LocationType.Zoom: return

Calendso will provide a Zoom meeting URL.

; } return null; }; const updateCustom = (e) => { e.preventDefault(); const customInput: EventTypeCustomInput = { label: e.target.label.value, placeholder: e.target.placeholder?.value, required: e.target.required.checked, type: e.target.type.value, }; if (selectedCustomInput) { selectedCustomInput.label = customInput.label; selectedCustomInput.placeholder = customInput.placeholder; selectedCustomInput.required = customInput.required; selectedCustomInput.type = customInput.type; } else { setCustomInputs(customInputs.concat(customInput)); } closeAddCustomModal(); }; const removeCustom = (index: number) => { customInputs.splice(index, 1); setCustomInputs([...customInputs]); }; return (
{eventType.title} | Event Type | Calendso } subtitle={eventType.description}>
{typeof location !== "undefined" ? location.hostname : ""}/{user.username}/
mins

{locations.length === 0 && (
{({ open }) => ( <> Show advanced settings
    {customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
  • Label: {customInput.label}
    {customInput.placeholder && (
    Placeholder: {customInput.placeholder}
    )}
    Type: {customInput.type}
    {customInput.required ? "Required" : "Optional"}
  • ))}

The booking needs to be manually confirmed before it is pushed to the integrations and a integrations and a confirmation mail is sent.


Date Range
{PERIOD_TYPES.map((period) => ( classnames( checked ? "border-secondary-200 z-10" : "border-gray-200", "relative min-h-14 flex items-center cursor-pointer focus:outline-none" ) }> {({ active, checked }) => ( <>
{period.prefix} {period.type === "rolling" && (
)} {checked && period.type === "range" && (
{ setPeriodStartDate(startDate); setPeriodEndDate(endDate); }} focusedInput={focusedInput} onFocusChange={(focusedInput) => { setFocusedInput(focusedInput); }} />
)} {period.suffix}
)}
))}

)}
Cancel
Delete Are you sure you want to delete this event type? Anyone who you've shared this link with will no longer be able to book using it.
{showLocationModal && (
{(selectedInputOption.value === EventTypeCustomInputType.TEXT || selectedInputOption.value === EventTypeCustomInputType.TEXTLONG) && (
)}
)}
); }; export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req, query } = context; const session = await getSession({ req }); if (!session) { return { redirect: { permanent: false, destination: "/auth/login", }, }; } const user: User = await prisma.user.findFirst({ where: { email: session.user.email, }, select: { username: true, timeZone: true, startTime: true, endTime: true, availability: true, }, }); const eventType: EventType | null = await prisma.eventType.findUnique({ where: { id: parseInt(query.type as string), }, select: { id: true, title: true, slug: true, description: true, length: true, hidden: true, locations: true, eventName: true, availability: true, customInputs: true, timeZone: true, periodType: true, periodDays: true, periodStartDate: true, periodEndDate: true, periodCountCalendarDays: true, requiresConfirmation: true, }, }); if (!eventType) { return { notFound: true, }; } const credentials = await prisma.credential.findMany({ where: { userId: user.id, }, select: { id: true, type: true, key: true, }, }); const integrations = [ { installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), enabled: credentials.find((integration) => integration.type === "google_calendar") != null, type: "google_calendar", title: "Google Calendar", imageSrc: "integrations/google-calendar.svg", 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, title: "Office 365 / Outlook.com Calendar", imageSrc: "integrations/outlook.svg", description: "For personal and business accounts", }, ]; 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" }); } 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. } const getAvailability = (providesAvailability) => providesAvailability.availability && providesAvailability.availability.length ? providesAvailability.availability : null; const availability: Availability[] = getAvailability(eventType) || getAvailability(user) || [ { days: [0, 1, 2, 3, 4, 5, 6], startTime: user.startTime, endTime: user.endTime, }, ]; availability.sort((a, b) => a.startTime - b.startTime); const eventTypeObject = Object.assign({}, eventType, { periodStartDate: eventType.periodStartDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null, }); return { props: { user, eventType: eventTypeObject, locationOptions, availability, }, }; }; export default EventTypePage;