import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { ChevronRightIcon, ClockIcon, DocumentIcon, ExternalLinkIcon, LinkIcon, LocationMarkerIcon, PencilIcon, PlusIcon, TrashIcon, UserAddIcon, UsersIcon, } from "@heroicons/react/solid"; import { Availability, EventTypeCustomInput, PeriodType, 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, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import Select from "react-select"; import { JSONObject } from "superjson/dist/types"; import { StripeData } from "@ee/lib/stripe/server"; import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; import { useLocale } from "@lib/hooks/useLocale"; import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations"; import { LocationType } from "@lib/location"; import showToast from "@lib/notification"; import prisma from "@lib/prisma"; import { defaultAvatarSrc } from "@lib/profile"; import { trpc } from "@lib/trpc"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import DestinationCalendarSelector from "@components/DestinationCalendarSelector"; import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog"; import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import { Form } from "@components/form/fields"; import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm"; import Button from "@components/ui/Button"; import InfoBadge from "@components/ui/InfoBadge"; import { Scheduler } from "@components/ui/Scheduler"; import Switch from "@components/ui/Switch"; import CheckboxField from "@components/ui/form/CheckboxField"; import CheckedSelect from "@components/ui/form/CheckedSelect"; import { DateRangePicker } from "@components/ui/form/DateRangePicker"; import MinutesField from "@components/ui/form/MinutesField"; import * as RadioArea from "@components/ui/form/radio-area"; import bloxyApi from "../../web3/dummyResps/bloxyApi"; dayjs.extend(utc); dayjs.extend(timezone); interface Token { name?: string; address: string; symbol: string; } interface NFT extends Token { // Some OpenSea NFTs have several contracts contracts: Array; } type AvailabilityInput = Pick; type OptionTypeBase = { label: string; value: LocationType; disabled?: boolean; }; const addDefaultLocationOptions = ( defaultLocations: OptionTypeBase[], locationOptions: OptionTypeBase[] ): void => { const existingLocationOptions = locationOptions.flatMap((locationOptionItem) => [locationOptionItem.value]); defaultLocations.map((item) => { if (!existingLocationOptions.includes(item.value)) { locationOptions.push(item); } }); }; const EventTypePage = (props: inferSSRProps) => { const { t } = useLocale(); const PERIOD_TYPES = [ { type: "ROLLING" as const, suffix: t("into_the_future"), }, { type: "RANGE" as const, prefix: t("within_date_range"), }, { type: "UNLIMITED" as const, prefix: t("indefinitely_into_future"), }, ]; const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } = props; /** Appending default locations */ const defaultLocations = [ { value: LocationType.InPerson, label: t("in_person_meeting") }, { value: LocationType.Phone, label: t("phone_call") }, ]; addDefaultLocationOptions(defaultLocations, locationOptions); const router = useRouter(); const updateMutation = trpc.useMutation("viewer.eventTypes.update", { onSuccess: async ({ eventType }) => { await router.push("/event-types"); showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success"); }, onError: (err) => { if (err instanceof HttpError) { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); } }, }); const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", { onSuccess: async () => { await router.push("/event-types"); showToast(t("event_type_deleted_successfully"), "success"); }, onError: (err) => { if (err instanceof HttpError) { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); } }, }); const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]); const [editIcon, setEditIcon] = useState(true); const [showLocationModal, setShowLocationModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); const [tokensList, setTokensList] = useState>([]); 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 [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false); useEffect(() => { const fetchTokens = async () => { // Get a list of most popular ERC20s and ERC777s, combine them into a single list, set as tokensList try { const erc20sList: Array = // await axios.get(`https://api.bloxy.info/token/list?key=${process.env.BLOXY_API_KEY}`) // ).data bloxyApi.slice(0, 100).map((erc20: Token) => { const { name, address, symbol } = erc20; return { name, address, symbol }; }); const exodiaList = await (await fetch(`https://exodia.io/api/trending?page=1`)).json(); const nftsList: Array = exodiaList.map((nft: NFT) => { const { name, contracts } = nft; if (nft.contracts[0]) { const { address, symbol } = contracts[0]; return { name, address, symbol }; } }); const unifiedList: Array = [...erc20sList, ...nftsList]; setTokensList(unifiedList); } catch (err) { showToast("Failed to load ERC20s & NFTs list. Please enter an address manually.", "error"); } }; console.log(tokensList); // Just here to make sure it passes the gc hook. Can remove once actual use is made of tokensList. fetchTokens(); }, []); useEffect(() => { setSelectedTimeZone(eventType.timeZone || ""); }, []); async function deleteEventTypeHandler(event: React.MouseEvent) { event.preventDefault(); const payload = { id: eventType.id }; deleteMutation.mutate(payload); } const openLocationModal = (type: LocationType) => { setSelectedLocation(locationOptions.find((option) => option.value === type)); setShowLocationModal(true); }; const removeLocation = (selectedLocation: typeof eventType.locations[number]) => { formMethods.setValue( "locations", formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type), { shouldValidate: true } ); }; const LocationOptions = () => { if (!selectedLocation) { return null; } switch (selectedLocation.value) { case LocationType.InPerson: return (
location.type === LocationType.InPerson)?.address } />
); case LocationType.Phone: return

{t("cal_invitee_phone_number_scheduling")}

; case LocationType.GoogleMeet: return

{t("cal_provide_google_meet_location")}

; case LocationType.Zoom: return

{t("cal_provide_zoom_meeting_url")}

; case LocationType.Daily: return

{t("cal_provide_video_meeting_url")}

; default: return null; } }; const removeCustom = (index: number) => { formMethods.getValues("customInputs").splice(index, 1); customInputs.splice(index, 1); setCustomInputs([...customInputs]); }; const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [ { value: SchedulingType.COLLECTIVE, label: t("collective"), description: t("collective_description"), }, { value: SchedulingType.ROUND_ROBIN, label: t("round_robin"), description: t("round_robin_description"), }, ]; const [periodDates] = useState<{ startDate: Date; endDate: Date }>({ startDate: new Date(eventType.periodStartDate || Date.now()), endDate: new Date(eventType.periodEndDate || Date.now()), }); const permalink = `${process.env.NEXT_PUBLIC_APP_URL}/${ team ? `team/${team.slug}` : eventType.users[0].username }/${eventType.slug}`; const mapUserToValue = ({ id, name, avatar, }: { id: number | null; name: string | null; avatar: string | null; }) => ({ value: `${id || ""}`, label: `${name || ""}`, avatar: `${avatar || ""}`, }); const formMethods = useForm<{ title: string; eventTitle: string; smartContractAddress: string; eventName: string; slug: string; length: number; description: string; disableGuests: boolean; requiresConfirmation: boolean; schedulingType: SchedulingType | null; price: number; hidden: boolean; locations: { type: LocationType; address?: string }[]; customInputs: EventTypeCustomInput[]; users: string[]; availability: { openingHours: AvailabilityInput[]; dateOverrides: AvailabilityInput[] }; timeZone: string; periodType: PeriodType; periodDays: number; periodCountCalendarDays: "1" | "0"; periodDates: { startDate: Date; endDate: Date }; minimumBookingNotice: number; slotInterval: number | null; destinationCalendar: { integration: string; externalId: string; }; }>({ 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 && (
)}
} subtitle={eventType.description || ""}>
{ const { periodDates, periodCountCalendarDays, smartContractAddress, ...input } = values; updateMutation.mutate({ ...input, periodStartDate: periodDates.startDate, periodEndDate: periodDates.endDate, periodCountCalendarDays: periodCountCalendarDays === "1", id: eventType.id, metadata: smartContractAddress ? { smartContractAddress, } : undefined, }); }} className="space-y-6">
{process.env.NEXT_PUBLIC_APP_URL?.replace(/^(https?:|)\/\//, "")}/ {team ? "team/" + team.slug : eventType.users[0].username}/
( {" "} {t("duration")} } id="length" required min="10" placeholder="15" defaultValue={eventType.length || 15} onChange={(e) => { formMethods.setValue("length", Number(e.target.value)); }} /> )} />

} />

{team &&
} {team && (
( { // FIXME // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore formMethods.setValue("schedulingType", val); }} /> )} />
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")} /> )} />
)} setAdvancedSettingsVisible(!advancedSettingsVisible)}> <> {t("show_advanced_settings")} {/** * Only display calendar selector if user has connected calendars AND if it's not * a team event. Since we don't have logic to handle each attende calendar (for now). * This will fallback to each user selected destination calendar. */} {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
( )} />
)}
{eventType.isWeb3Active && (
{ }
)}
    {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")}
  • ))}
( { formMethods.setValue("requiresConfirmation", e?.target.checked); }} /> )} /> ( { formMethods.setValue("disableGuests", e?.target.checked); }} /> )} />
( { formMethods.setValue("minimumBookingNotice", Number(e.target.value)); }} /> )} />
{ const slotIntervalOptions = [ { label: t("slot_interval_default"), value: -1, }, ...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({ label: minutes + " " + t("minutes"), value: minutes, })), ]; return (
)} {period.type === "RANGE" && (
( { formMethods.setValue("periodDates", { startDate, endDate }); }} /> )} />
)} {period.suffix ? (  {period.suffix} ) : null}
))} )} />

( { formMethods.setValue("availability", { openingHours: val.openingHours, dateOverrides: val.dateOverrides, }); }} setTimeZone={(timeZone) => { formMethods.setValue("timeZone", timeZone); setSelectedTimeZone(timeZone); }} timeZone={selectedTimeZone} availability={availability.map((schedule) => ({ ...schedule, startTime: new Date(schedule.startTime), endTime: new Date(schedule.endTime), }))} /> )} />
{hasPaymentIntegration && ( <>
{ setRequirePayment(event.target.checked); if (!event.target.checked) { formMethods.setValue("price", 0); } }} 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 && (
( { field.onChange(e.target.valueAsNumber * 100); }} value={field.value > 0 ? field.value / 100 : 0} /> )} />
{new Intl.NumberFormat("en", { style: "currency", currency: currency, maximumSignificantDigits: 1, maximumFractionDigits: 0, }) .format(0) .replace("0", "")}
)}
)} {/* )} */}
( { formMethods.setValue("hidden", isChecked); }} label={t("hide_event_type")} /> )} />
{t("delete")} {t("delete_event_type_description")}

{t("this_input_will_shown_booking_this_event")}

{ const newLocation = values.locationType; let details = {}; if (newLocation === LocationType.InPerson) { details = { address: values.locationAddress }; } const existingIdx = formMethods .getValues("locations") .findIndex((loc) => values.locationType === loc.type); if (existingIdx !== -1) { const copy = formMethods.getValues("locations"); copy[existingIdx] = { ...formMethods.getValues("locations")[existingIdx], ...details }; formMethods.setValue("locations", copy); } else { formMethods.setValue( "locations", formMethods.getValues("locations").concat({ type: values.locationType, ...details }) ); } setShowLocationModal(false); }}> (