diff --git a/components/ui/form/MinutesField.tsx b/components/ui/form/MinutesField.tsx index 50cd3caf..c3fba5de 100644 --- a/components/ui/form/MinutesField.tsx +++ b/components/ui/form/MinutesField.tsx @@ -8,7 +8,7 @@ const MinutesField = forwardRef(({ label, ...rest }, re return (
-
diff --git a/lib/types/event-type.ts b/lib/types/event-type.ts index ad0e4134..b352a1e3 100644 --- a/lib/types/event-type.ts +++ b/lib/types/event-type.ts @@ -15,15 +15,10 @@ export type AdvancedOptions = { price?: number; currency?: string; schedulingType?: SchedulingType; - users?: { - value: number; - label: string; - avatar: string; - }[]; + users?: string[]; availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }; customInputs?: EventTypeCustomInput[]; - timeZone: string; - hidden: boolean; + timeZone?: string; }; export type EventTypeCustomInput = { @@ -55,7 +50,5 @@ export type EventTypeInput = AdvancedOptions & { length: number; hidden: boolean; locations: unknown; - customInputs: EventTypeCustomInput[]; - timeZone: string; availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }; }; diff --git a/package.json b/package.json index 04714d87..d84f50ad 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-dialog": "^0.1.0", "@radix-ui/react-dropdown-menu": "^0.1.1", "@radix-ui/react-id": "^0.1.0", + "@radix-ui/react-radio-group": "^0.1.1", "@radix-ui/react-slider": "^0.1.1", "@radix-ui/react-switch": "^0.1.1", "@radix-ui/react-tooltip": "^0.1.0", diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index 169162b8..70ae3ca8 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -1,11 +1,9 @@ -// TODO: replace headlessui with radix-ui -import { Disclosure, RadioGroup } from "@headlessui/react"; import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { ChevronRightIcon, - ClockIcon, DocumentIcon, ExternalLinkIcon, + ClockIcon, LinkIcon, LocationMarkerIcon, PencilIcon, @@ -15,26 +13,23 @@ import { UsersIcon, } from "@heroicons/react/solid"; import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; +import * as RadioGroup from "@radix-ui/react-radio-group"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import { useMutation } from "react-query"; import Select, { OptionTypeBase } from "react-select"; import { StripeData } from "@ee/lib/stripe/server"; -import { - asNumberOrThrow, - asNumberOrUndefined, - asStringOrThrow, - asStringOrUndefined, -} from "@lib/asStringOrNull"; +import { asNumberOrUndefined, asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; -import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; import { useLocale } from "@lib/hooks/useLocale"; import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations"; @@ -68,15 +63,15 @@ const EventTypePage = (props: inferSSRProps) => { const { t } = useLocale(); const PERIOD_TYPES = [ { - type: "rolling", + type: "ROLLING" as const, suffix: t("into_the_future"), }, { - type: "range", + type: "RANGE" as const, prefix: t("within_date_range"), }, { - type: "unlimited", + type: "UNLIMITED" as const, prefix: t("indefinitely_into_future"), }, ]; @@ -111,91 +106,27 @@ const EventTypePage = (props: inferSSRProps) => { }, }); - const [users, setUsers] = useState([]); const [editIcon, setEditIcon] = useState(true); - const [enteredAvailability, setEnteredAvailability] = useState<{ - openingHours: WorkingHours[]; - dateOverrides: WorkingHours[]; - }>(); const [showLocationModal, setShowLocationModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); - const [locations, setLocations] = useState(eventType.locations || []); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); - const [periodType, setPeriodType] = useState(() => { - return ( - PERIOD_TYPES.find((s) => s.type === eventType.periodType) || - PERIOD_TYPES.find((s) => s.type === "unlimited") - ); - }); + const periodType = + PERIOD_TYPES.find((s) => s.type === eventType.periodType) || + PERIOD_TYPES.find((s) => s.type === "UNLIMITED"); + const [requirePayment, setRequirePayment] = useState(eventType.price > 0); - - const [hidden, setHidden] = useState(eventType.hidden); - - const titleRef = useRef(null); - const eventNameRef = useRef(null); const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false); useEffect(() => { setSelectedTimeZone(eventType.timeZone || ""); }, []); - async function updateEventTypeHandler(event: React.FormEvent) { - event.preventDefault(); - - const formData = Object.fromEntries(new FormData(event.currentTarget).entries()); - - const enteredTitle: string = titleRef.current!.value; - - const advancedPayload: AdvancedOptions = {}; - if (advancedSettingsVisible) { - advancedPayload.eventName = eventNameRef.current.value; - advancedPayload.periodType = periodType?.type; - advancedPayload.periodDays = asNumberOrUndefined(formData.periodDays); - advancedPayload.periodCountCalendarDays = Boolean( - asNumberOrUndefined(formData.periodCountCalendarDays) - ); - advancedPayload.periodStartDate = periodDates.startDate || undefined; - advancedPayload.periodEndDate = periodDates.endDate || undefined; - advancedPayload.minimumBookingNotice = asNumberOrUndefined(formData.minimumBookingNotice); - // prettier-ignore - advancedPayload.price = - !requirePayment ? undefined : - formData.price ? Math.round(parseFloat(asStringOrThrow(formData.price)) * 100) : - /* otherwise */ 0; - advancedPayload.currency = currency; - advancedPayload.availability = enteredAvailability || undefined; - advancedPayload.customInputs = customInputs; - advancedPayload.timeZone = selectedTimeZone; - advancedPayload.disableGuests = formData.disableGuests === "on"; - advancedPayload.requiresConfirmation = formData.requiresConfirmation === "on"; - } - - const payload: EventTypeInput = { - id: eventType.id, - title: enteredTitle, - slug: asStringOrThrow(formData.slug), - description: asStringOrThrow(formData.description), - length: asNumberOrThrow(formData.length), - hidden, - locations, - ...advancedPayload, - ...(team - ? { - schedulingType: formData.schedulingType as SchedulingType, - users, - } - : {}), - }; - - updateMutation.mutate(payload); - } - async function deleteEventTypeHandler(event: React.MouseEvent) { event.preventDefault(); @@ -208,34 +139,12 @@ const EventTypePage = (props: inferSSRProps) => { setShowLocationModal(true); }; - const closeLocationModal = () => { - setSelectedLocation(undefined); - setShowLocationModal(false); - }; - - const updateLocations = (e: React.FormEvent) => { - e.preventDefault(); - const newLocation = e.currentTarget.location.value; - - let details = {}; - if (newLocation === LocationType.InPerson) { - details = { address: e.currentTarget.address.value }; - } - - const existingIdx = locations.findIndex((loc) => newLocation === loc.type); - if (existingIdx !== -1) { - const copy = locations; - copy[existingIdx] = { ...locations[existingIdx], ...details }; - setLocations(copy); - } else { - setLocations(locations.concat({ type: newLocation, ...details })); - } - - setShowLocationModal(false); - }; - const removeLocation = (selectedLocation: typeof eventType.locations[number]) => { - setLocations(locations.filter((location) => location.type !== selectedLocation.type)); + formMethods.setValue( + "locations", + formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type), + { shouldValidate: true } + ); }; const LocationOptions = () => { @@ -252,11 +161,15 @@ const EventTypePage = (props: inferSSRProps) => {
location.type === LocationType.InPerson)?.address} + defaultValue={ + formMethods + .getValues("locations") + .find((location) => location.type === LocationType.InPerson)?.address + } />
@@ -274,6 +187,7 @@ const EventTypePage = (props: inferSSRProps) => { }; const removeCustom = (index: number) => { + formMethods.getValues("customInputs").splice(index, 1); customInputs.splice(index, 1); setCustomInputs([...customInputs]); }; @@ -291,7 +205,7 @@ const EventTypePage = (props: inferSSRProps) => { }, ]; - const [periodDates, setPeriodDates] = useState<{ startDate: Date; endDate: Date }>({ + const [periodDates] = useState<{ startDate: Date; endDate: Date }>({ startDate: new Date(eventType.periodStartDate || Date.now()), endDate: new Date(eventType.periodEndDate || Date.now()), }); @@ -314,6 +228,215 @@ const EventTypePage = (props: inferSSRProps) => { avatar: `${avatar || ""}`, }); + const formMethods = useForm<{ + title: string; + eventTitle: string; + slug: string; + length: number; + description: string; + disableGuests: boolean; + requiresConfirmation: boolean; + schedulingType: SchedulingType | null; + price: number; + isHidden: boolean; + locations: { type: LocationType; address?: string }[]; + customInputs: EventTypeCustomInput[]; + users: string[]; + scheduler: { + enteredAvailability: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }; + selectedTimezone: string; + }; + periodType: string | number; + periodDays: number; + periodDaysType: string; + periodDates: { startDate: Date; endDate: Date }; + minimumBookingNotice: number; + }>({ + defaultValues: { + locations: eventType.locations || [], + periodDates: { + startDate: periodDates.startDate, + endDate: periodDates.endDate, + }, + }, + }); + + const locationFormMethods = useForm<{ + locationType: LocationType; + locationAddress: string; + }>(); + + const Locations = () => { + return ( +
+ {formMethods.getValues("locations").length === 0 && ( +
+ {editIcon && ( @@ -343,7 +464,51 @@ const EventTypePage = (props: inferSSRProps) => {
-
+ { + const enteredTitle: string = values.title; + + const advancedPayload: AdvancedOptions = {}; + if (advancedSettingsVisible) { + advancedPayload.eventName = values.eventTitle; + advancedPayload.periodType = asStringOrUndefined(values.periodType); + advancedPayload.periodDays = asNumberOrUndefined(values.periodDays); + advancedPayload.periodCountCalendarDays = Boolean(parseInt(values.periodDaysType)); + advancedPayload.periodStartDate = values.periodDates.startDate || undefined; + advancedPayload.periodEndDate = values.periodDates.endDate || undefined; + advancedPayload.minimumBookingNotice = values.minimumBookingNotice; + // prettier-ignore + advancedPayload.price = + !requirePayment ? undefined : + values.price ? Math.round(parseFloat(asStringOrThrow(values.price)) * 100) : + /* otherwise */ 0; + advancedPayload.currency = currency; // + advancedPayload.availability = values.scheduler.enteredAvailability || undefined; + advancedPayload.customInputs = values.customInputs; + advancedPayload.timeZone = values.scheduler.selectedTimezone; + advancedPayload.disableGuests = values.disableGuests; + advancedPayload.requiresConfirmation = values.requiresConfirmation; + } + + const payload: EventTypeInput = { + id: eventType.id, + title: enteredTitle, + slug: asStringOrThrow(values.slug), + description: asStringOrThrow(values.description), + length: values.length, + hidden: values.isHidden, + locations: values.locations, + ...advancedPayload, + ...(team + ? { + schedulingType: values.schedulingType as SchedulingType, + users: values.users, + } + : {}), + }; + updateMutation.mutate(payload); + })} + className="space-y-6">
@@ -360,27 +525,34 @@ const EventTypePage = (props: inferSSRProps) => {
- - - {t("duration")} - - } + ( + + {t("duration")} + + } + id="length" + required + placeholder="15" + defaultValue={eventType.length || 15} + onChange={(e) => { + formMethods.setValue("length", Number(e.target.value)); + }} + /> + )} />

@@ -392,172 +564,12 @@ const EventTypePage = (props: inferSSRProps) => { {t("location")}
-
- {locations.length === 0 && ( -
-
@@ -590,10 +602,19 @@ const EventTypePage = (props: inferSSRProps) => { {t("scheduling_type")}
- ( + { + formMethods.setValue("schedulingType", val); + }} + /> + )} />
@@ -604,352 +625,390 @@ const EventTypePage = (props: inferSSRProps) => {
- setUsers(options.map((option) => option.value))} - defaultValue={eventType.users.map(mapUserToValue)} - options={teamMembers.map(mapUserToValue)} - id="users" - placeholder={t("add_attendees")} + user.id.toString())} + render={() => ( + { + formMethods.setValue( + "users", + options.map((user) => user.value) + ); + }} + defaultValue={eventType.users.map(mapUserToValue)} + options={teamMembers.map(mapUserToValue)} + placeholder={t("add_attendees")} + /> + )} />
)} - - - {({ open }) => ( - <> -
setAdvancedSettingsVisible(!advancedSettingsVisible)}> - - - - {t("show_advanced_settings")} - - -
- -
-
- -
-
-
- -
-
+ setAdvancedSettingsVisible(!advancedSettingsVisible)}> + <> + + + + {t("show_advanced_settings")} + + + +
+
+
-
-
- -
-
-
    - {customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( -
  • -
    -
    -
    - - {t("label")}: {customInput.label} - -
    - {customInput.placeholder && ( -
    - - {t("placeholder")}: {customInput.placeholder} - -
    - )} -
    - - {t("type")}: {customInput.type} - -
    -
    - - {customInput.required ? t("required") : t("optional")} - -
    -
    -
    - - -
    -
    -
  • - ))} -
  • - -
  • -
-
-
- - - - - -
- - - -
-
- -
-
- - {t("date_range")} -
- {PERIOD_TYPES.map((period) => ( - - classNames( - checked ? "border-secondary-200 z-10" : "border-gray-200", - "relative min-h-12 flex items-center cursor-pointer focus:outline-none" - ) - }> - {({ active, checked }) => ( - <> - -
- - {period.prefix ? {period.prefix}  : null} - {period.type === "rolling" && ( -
- - -
- )} - - {checked && period.type === "range" && ( -
- -
- )} - {period.suffix ?  {period.suffix} : null} -
-
- - )} -
- ))} -
-
-
-
- -
- -
-
- -
-
- ({ - ...schedule, - startTime: new Date(schedule.startTime), - endTime: new Date(schedule.endTime), - }))} +
+
+
+
+
+
+ +
+
+
    + {customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( +
  • +
    +
    +
    + + {t("label")}: {customInput.label} + +
    + {customInput.placeholder && ( +
    + + {t("placeholder")}: {customInput.placeholder} + +
    + )} +
    + + {t("type")}: {customInput.type} + +
    +
    + + {customInput.required ? t("required") : t("optional")} + +
    +
    +
    + + +
    +
    +
  • + ))} +
  • + +
  • +
+
+
- {hasPaymentIntegration && ( - <> -
-
-
- + ( + { + formMethods.setValue("requiresConfirmation", e?.target.checked); + }} + /> + )} + /> + + ( + { + formMethods.setValue("disableGuests", e?.target.checked); + }} + /> + )} + /> + +
+ ( + { + formMethods.setValue("minimumBookingNotice", Number(e.target.value)); + }} + /> + )} + /> + +
+
+ +
+
+ ( + formMethods.setValue("periodType", val)}> + {PERIOD_TYPES.map((period) => ( +
+ + + + {period.prefix ? {period.prefix}  : null} + {period.type === "ROLLING" && ( +
+ { + formMethods.setValue("periodDays", Number(e.target.value)); + }} + /> + +
+ )} + {period.type === "RANGE" && ( +
+ ( + { + formMethods.setValue("periodDates", { startDate, endDate }); + }} + /> + )} + /> +
+ )} + {period.suffix ? ( +  {period.suffix} + ) : null} +
+ ))} +
+ )} + /> +
+
+ +
+ +
+
+ +
+
+ ( + { + formMethods.setValue("scheduler.enteredAvailability", { + openingHours: val.openingHours, + dateOverrides: val.dateOverrides, + }); + }} + setTimeZone={(val: string) => { + formMethods.setValue("scheduler.selectedTimezone", val); + }} + timeZone={selectedTimeZone} + availability={availability.map((schedule) => ({ + ...schedule, + startTime: new Date(schedule.startTime), + endTime: new Date(schedule.endTime), + }))} + /> + )} + /> +
+
+ + {hasPaymentIntegration && ( + <> +
+
+
+ +
+ +
+
+
+
+
+
+ setRequirePayment(event.target.checked)} + id="requirePayment" + name="requirePayment" + type="checkbox" + className="w-4 h-4 border-gray-300 rounded focus:ring-primary-500 text-primary-600" + defaultChecked={requirePayment} + /> +
+
+

+ {t("require_payment")} (0.5% +{" "} + + + {" "} + {t("commission_per_transaction")}) +

+
+
+
+
- -
+ {requirePayment && (
-
-
- setRequirePayment(event.target.checked)} - id="requirePayment" - name="requirePayment" - type="checkbox" - className="w-4 h-4 border-gray-300 rounded focus:ring-primary-500 text-primary-600" - defaultChecked={requirePayment} - /> -
-
-

- {t("require_payment")} (0.5% +{" "} - - - {" "} - {t("commission_per_transaction")}) -

+
+ 0 ? eventType.price / 100.0 : undefined + } + {...formMethods.register("price")} + /> +
+ + {new Intl.NumberFormat("en", { + style: "currency", + currency: currency, + maximumSignificantDigits: 1, + maximumFractionDigits: 0, + }) + .format(0) + .replace("0", "")} +
- {requirePayment && ( -
-
-
-
- 0 ? eventType.price / 100.0 : undefined - } - /> -
- - {new Intl.NumberFormat("en", { - style: "currency", - currency: currency, - maximumSignificantDigits: 1, - maximumFractionDigits: 0, - }) - .format(0) - .replace("0", "")} - -
-
-
-
-
- )} -
+ )}
- - )} - - - )} - +
+ + )} + + + {/* )} */} +
- ( + { + formMethods.setValue("isHidden", isChecked); + }} + label={t("hide_event_type")} + /> + )} />
@@ -973,7 +1040,7 @@ const EventTypePage = (props: inferSSRProps) => { href={permalink} target="_blank" rel="noreferrer" - className="flex inline-flex items-center px-2 py-1 text-sm font-medium rounded-sm text-md text-neutral-700 hover:text-gray-900 hover:bg-gray-200"> + className="inline-flex items-center px-2 py-1 text-sm font-medium rounded-sm text-md text-neutral-700 hover:text-gray-900 hover:bg-gray-200">
- {showLocationModal && ( -
-
- - - - -
-
-
- -
-
- -
-
- - { + locationFormMethods.setValue("locationType", val.value); + setSelectedLocation(val); + }} + /> + )} + /> + +
+ + +
+
+ a.id - b.id) || []} + render={() => ( + + +
+
+
+ +
+
+ +
+

+ {t("this_input_will_shown_booking_this_event")} +

+
+
+
+ { + const customInput: EventTypeCustomInput = { + id: -1, + eventTypeId: -1, + label: values.label, + placeholder: values.placeholder, + required: values.required, + type: values.type, + }; + + if (selectedCustomInput) { + selectedCustomInput.label = customInput.label; + selectedCustomInput.placeholder = customInput.placeholder; + selectedCustomInput.required = customInput.required; + selectedCustomInput.type = customInput.type; + } else { + setCustomInputs(customInputs.concat(customInput)); + formMethods.setValue( + "customInputs", + formMethods.getValues("customInputs").concat(customInput) + ); + } + setSelectedCustomInputModalOpen(false); + }} + onCancel={() => { + setSelectedCustomInputModalOpen(false); + }} + /> +
+
+
+ )} + />
); diff --git a/yarn.lock b/yarn.lock index c0453588..bc33186e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1482,6 +1482,17 @@ "@radix-ui/react-id" "0.1.1" "@radix-ui/react-primitive" "0.1.1" +"@radix-ui/react-label@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.1.1.tgz#c2970b19214248c2b3a0425c3c0d299290b559a5" + integrity sha512-52mHm7gxDcbY1+XuFwe0zBvUHp+JP424QC5V2nloPH9JUpCsM2MfviqA/nyW4nKuoGAeF6MhedjtlrXyze8DFw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.1.0" + "@radix-ui/react-context" "0.1.1" + "@radix-ui/react-id" "0.1.1" + "@radix-ui/react-primitive" "0.1.1" + "@radix-ui/react-menu@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.1.1.tgz#2146352813ac086df5f021d06bce10f7f56d2577" @@ -1547,6 +1558,23 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "0.1.1" +"@radix-ui/react-radio-group@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.1.1.tgz#e46861abd472f52ed57c8379e4e8301bbc503ed1" + integrity sha512-K6vrFSI62qEnF6ltlyK0pzY9w/Y/HnmheUFcHSfWpyyBU6vmoU/Vdy1ZDAejDtDfdthSrk/L8wczF1OPmIjB2w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.1.0" + "@radix-ui/react-compose-refs" "0.1.0" + "@radix-ui/react-context" "0.1.1" + "@radix-ui/react-label" "0.1.1" + "@radix-ui/react-presence" "0.1.1" + "@radix-ui/react-primitive" "0.1.1" + "@radix-ui/react-roving-focus" "0.1.1" + "@radix-ui/react-use-controllable-state" "0.1.0" + "@radix-ui/react-use-previous" "0.1.0" + "@radix-ui/react-use-size" "0.1.0" + "@radix-ui/react-roving-focus@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.1.tgz#6a7965f6315fae91061b14d6380949a4697e87b9"