import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, 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 } from "next-auth/client"; import { Scheduler } from "@components/ui/Scheduler"; import { Disclosure } from "@headlessui/react"; import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput"; import { LocationMarkerIcon, LinkIcon, PencilIcon, 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 { RadioGroup } from "@headlessui/react"; 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"; dayjs.extend(utc); dayjs.extend(timezone); type Props = { user: User; eventType: EventType; locationOptions: OptionBase[]; availability: Availability[]; }; type OpeningHours = { days: number[]; startTime: number; endTime: number; }; type DateOverride = { date: string; startTime: number; endTime: number; }; type EventTypeInput = { id: number; title: string; slug: string; description: string; length: number; hidden: boolean; locations: unknown; eventName: string; customInputs: EventTypeCustomInput[]; timeZone: string; availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] }; periodType?: string; periodDays?: number; periodStartDate?: Date | string; periodEndDate?: Date | string; periodCountCalendarDays?: boolean; enteredRequiresConfirmation: boolean; }; const PERIOD_TYPES = [ { type: "rolling", suffix: "into the future", }, { type: "range", prefix: "Within a date range", }, { type: "unlimited", prefix: "Indefinitely into the future", }, ]; export default function EventTypePage({ user, eventType, locationOptions, availability, }: Props): JSX.Element { const router = useRouter(); console.log(eventType); 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 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 titleRef = useRef(); const slugRef = useRef(); const descriptionRef = useRef(); const lengthRef = useRef(); const isHiddenRef = 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 enteredIsHidden: boolean = isHiddenRef.current.checked; const enteredRequiresConfirmation: boolean = requiresConfirmationRef.current.checked; const enteredEventName: string = eventNameRef.current.value; const type = periodType.type; const enteredPeriodDays = parseInt(periodDaysRef?.current?.value); const enteredPeriodDaysType = Boolean(parseInt(periodDaysTypeRef?.current.value)); const enteredPeriodStartDate = periodStartDate ? periodStartDate.toDate() : null; const enteredPeriodEndDate = periodEndDate ? periodEndDate.toDate() : null; console.log("values", { type, periodDaysTypeRef, enteredPeriodDays, enteredPeriodDaysType, enteredPeriodStartDate, enteredPeriodEndDate, }); // TODO: Add validation const payload: EventTypeInput = { id: eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden, locations, eventName: enteredEventName, customInputs, timeZone: selectedTimeZone, periodType: type, periodDays: enteredPeriodDays, periodStartDate: enteredPeriodStartDate, periodEndDate: enteredPeriodEndDate, periodCountCalendarDays: enteredPeriodDaysType, requiresConfirmation: enteredRequiresConfirmation, }; if (enteredAvailability) { payload.availability = enteredAvailability; } await fetch("/api/availability/eventtype", { method: "PATCH", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", }, }); router.push("/availability"); } async function deleteEventTypeHandler(event) { event.preventDefault(); await fetch("/api/availability/eventtype", { method: "DELETE", body: JSON.stringify({ id: eventType.id }), headers: { "Content-Type": "application/json", }, }); router.push("/availability"); } 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 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, required: e.target.required.checked, type: e.target.type.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; input.required = customInput.required; input.type = customInput.type; setCustomInputs(customInputs); } } else { setCustomInputs(customInputs.concat(customInput)); } closeAddCustomModal(); }; const removeCustom = (customInput, e) => { e.preventDefault(); const index = customInputs.findIndex((inp) => inp.id === customInput.id); if (index >= 0) { customInputs.splice(index, 1); setCustomInputs([...customInputs]); } }; return (
{eventType.title} | Event Type | Calendso
{typeof location !== "undefined" ? location.hostname : ""}/{user.username}/
{locations.length === 0 && (
mins
{({ open }) => ( <> Show advanced settings
    {customInputs.map((customInput) => (
  • Label: {customInput.label}
    Type: {customInput.type}
    {customInput.required ? "Required" : "Optional"}
  • ))}

Hide the event type from your page, so it can only be booked through its URL.

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
{showLocationModal && (
)}
); } export const getServerSideProps: GetServerSideProps = async ({ req, query }) => { 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, }, }; };