From 3a5e7dd61ccf0056a3c0f5b0e4ceef86d425c2e8 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Sat, 14 Aug 2021 16:43:34 +0000 Subject: [PATCH 1/3] Delete old redundant page --- pages/availability/event/[type].tsx | 1127 --------------------------- 1 file changed, 1127 deletions(-) delete mode 100644 pages/availability/event/[type].tsx diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx deleted file mode 100644 index bc88c293fa..0000000000 --- a/pages/availability/event/[type].tsx +++ /dev/null @@ -1,1127 +0,0 @@ -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 { LocationMarkerIcon, PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline"; -import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput"; -import { PlusIcon } 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 Text from "@components/ui/Text"; -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; - requiresConfirmation: boolean; - minimumBookingNotice: number; -}; - -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(); - - 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 minimumBookingNoticeRef = 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 enteredMinimumBookingNotice: number = parseInt(minimumBookingNoticeRef.current.value); - 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; - - // 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, - minimumBookingNotice: enteredMinimumBookingNotice, - 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 && ( -
-
- -
-
-
- -
- -
-
-
- -
    - {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 confirmed, before it is pushed to the integrations and a - confirmation mail is sent. -

-
-
-
- -
- When can people book this event? -
- -
- -
- minutes -
-
-
-
-
-
- {/* */} - Invitees can schedule... -
- - Date Range -
- {PERIOD_TYPES.map((period) => ( - - classnames( - checked ? "bg-indigo-50 border-indigo-200 z-10" : "border-gray-200", - "relative py-4 px-2 lg:p-4 min-h-20 lg:flex items-center cursor-pointer focus:outline-none" - ) - }> - {({ active, checked }) => ( - <> - - ))} -
-
-
-
-
-
- -
- -
- minutes -
-
-
-
-
-

- How do you want to offer your availability for this event type? -

- -
- - Cancel - - -
-
-
-
- -
-
-
-
-

Delete this event type

-
-

Once you delete this event type, it will be permanently removed.

-
-
- -
-
-
-
-
- {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, - minimumBookingNotice: 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, - }, - }; -}; From 252a329f09883fa74f8a7fd74cc1d086bccab5a8 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Sat, 14 Aug 2021 17:03:50 +0000 Subject: [PATCH 2/3] Fixed issues relating to custom-inputs * Don't duplicate custom input when editing before db persist * Remove correct custom input during delete pre db persist (id undefined) * Moved typings to prisma, keeping backwards compatibility with @map * Updated all usages of the enum --- lib/eventTypeInput.ts | 13 ------------ pages/[user]/book.tsx | 14 ++++++------- pages/event-types/[type].tsx | 39 ++++++++++++++---------------------- prisma/schema.prisma | 9 ++++++++- 4 files changed, 30 insertions(+), 45 deletions(-) delete mode 100644 lib/eventTypeInput.ts diff --git a/lib/eventTypeInput.ts b/lib/eventTypeInput.ts deleted file mode 100644 index e8c76e428c..0000000000 --- a/lib/eventTypeInput.ts +++ /dev/null @@ -1,13 +0,0 @@ -export enum EventTypeCustomInputType { - Text = 'text', - TextLong = 'textLong', - Number = 'number', - Bool = 'bool', -} - -export interface EventTypeCustomInput { - id?: number; - type: EventTypeCustomInputType; - label: string; - required: boolean; -} diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 74e2c78536..92a27ef98c 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid"; import prisma, { whereAndSelect } from "../../lib/prisma"; +import { EventTypeCustomInputType } from "@prisma/client"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; import { useEffect, useState } from "react"; import dayjs from "dayjs"; @@ -13,7 +14,6 @@ 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 Theme from "@components/Theme"; import { ReactMultiEmail } from "react-multi-email"; import "react-multi-email/style.css"; @@ -71,7 +71,7 @@ export default function Book(props: any): JSX.Element { .map((input) => { const data = event.target["custom_" + input.id]; if (data) { - if (input.type === EventTypeCustomInputType.Bool) { + if (input.type === EventTypeCustomInputType.BOOL) { return input.label + "\n" + (data.checked ? "Yes" : "No"); } else { return input.label + "\n" + data.value; @@ -273,14 +273,14 @@ export default function Book(props: any): JSX.Element { .sort((a, b) => a.id - b.id) .map((input) => (
- {input.type !== EventTypeCustomInputType.Bool && ( + {input.type !== EventTypeCustomInputType.BOOL && ( )} - {input.type === EventTypeCustomInputType.TextLong && ( + {input.type === EventTypeCustomInputType.TEXTLONG && (