diff --git a/README.md b/README.md index 26d7519cf4..9916849add 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If yarn test-e2e # To open last HTML report run: -yarn playwright show-report test-results/reports/playwright-html-report +yarn playwright show-report test-results/reports/playwright-html-report ``` ### Upgrading from earlier versions diff --git a/apps/web/components/DestinationCalendarSelector.tsx b/apps/web/components/DestinationCalendarSelector.tsx index 33fe386e80..90b18037be 100644 --- a/apps/web/components/DestinationCalendarSelector.tsx +++ b/apps/web/components/DestinationCalendarSelector.tsx @@ -107,9 +107,10 @@ const DestinationCalendarSelector = ({ control: (defaultStyles) => { return { ...defaultStyles, - borderRadius: "2px", + borderRadius: "6px", "@media only screen and (min-width: 640px)": { ...(defaultStyles["@media only screen and (min-width: 640px)"] as object), + width: "100%", }, }; }, @@ -135,6 +136,9 @@ const DestinationCalendarSelector = ({ }} isLoading={isLoading} value={selectedOption} + components={{ + IndicatorSeparator: () => null, + }} /> ); diff --git a/apps/web/components/availability/Schedule.tsx b/apps/web/components/availability/Schedule.tsx index 1d03fe56fa..53020d52ec 100644 --- a/apps/web/components/availability/Schedule.tsx +++ b/apps/web/components/availability/Schedule.tsx @@ -1,3 +1,7 @@ +/** + * @deprecated + * use Component in "/packages/features/schedules/components/Schedule"; + **/ import classNames from "classnames"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Controller, useFieldArray, useFormContext } from "react-hook-form"; @@ -192,6 +196,10 @@ const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (select ); }; +/** + * @deprecated + * use Component in "/packages/features/schedules/components/Schedule"; + **/ export const DayRanges = ({ name, defaultValue = [defaultDayRange], @@ -324,6 +332,10 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { ); }; +/** + * @deprecated + * use Component in "/packages/features/schedules/components/Schedule"; + **/ const Schedule = ({ name }: { name: string }) => { const { i18n } = useLocale(); return ( diff --git a/apps/web/components/availability/ScheduleListItem.tsx b/apps/web/components/availability/ScheduleListItem.tsx index d32b56db07..0797cdd8a1 100644 --- a/apps/web/components/availability/ScheduleListItem.tsx +++ b/apps/web/components/availability/ScheduleListItem.tsx @@ -1,6 +1,6 @@ /** * @deprecated modifications to this file should be v2 only - * Use `/packages/ui/modules/availability/ScheduleListItem.tsx` instead + * Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead */ import Link from "next/link"; import { Fragment } from "react"; @@ -13,6 +13,10 @@ import { Button } from "@calcom/ui"; import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown"; import { Icon } from "@calcom/ui/Icon"; +/** + * @deprecated modifications to this file should be v2 only + * Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead + */ export function ScheduleListItem({ schedule, deleteFunction, diff --git a/apps/web/components/getting-started/components/CalendarItem.tsx b/apps/web/components/getting-started/components/CalendarItem.tsx new file mode 100644 index 0000000000..ed63c6063c --- /dev/null +++ b/apps/web/components/getting-started/components/CalendarItem.tsx @@ -0,0 +1,42 @@ +import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/components"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { App } from "@calcom/types/App"; +import Button from "@calcom/ui/v2/core/Button"; + +interface ICalendarItem { + title: string; + description?: string; + imageSrc: string; + type: App["type"]; +} + +const CalendarItem = (props: ICalendarItem) => { + const { title, imageSrc, type } = props; + const { t } = useLocale(); + return ( +
+ {title} +

{title}

+ + ( + + )} + /> +
+ ); +}; + +export { CalendarItem }; diff --git a/apps/web/components/getting-started/components/CalendarSwitch.tsx b/apps/web/components/getting-started/components/CalendarSwitch.tsx new file mode 100644 index 0000000000..04aa303e01 --- /dev/null +++ b/apps/web/components/getting-started/components/CalendarSwitch.tsx @@ -0,0 +1,83 @@ +import { useMutation } from "react-query"; + +import showToast from "@calcom/lib/notification"; +import { trpc } from "@calcom/trpc/react"; +import { Switch } from "@calcom/ui/v2"; + +interface ICalendarSwitchProps { + title: string; + externalId: string; + type: string; + isChecked: boolean; + name: string; +} +const CalendarSwitch = (props: ICalendarSwitchProps) => { + const { title, externalId, type, isChecked, name } = props; + const utils = trpc.useContext(); + const mutation = useMutation< + unknown, + unknown, + { + isOn: boolean; + } + >( + async ({ isOn }) => { + const body = { + integration: type, + externalId: externalId, + }; + + if (isOn) { + const res = await fetch("/api/availability/calendar", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + throw new Error("Something went wrong"); + } + } else { + const res = await fetch("/api/availability/calendar", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + throw new Error("Something went wrong"); + } + } + }, + { + async onSettled() { + await utils.invalidateQueries(["viewer.integrations"]); + }, + onError() { + showToast(`Something went wrong when toggling "${title}""`, "error"); + }, + } + ); + return ( +
+
+ { + mutation.mutate({ isOn }); + }} + /> +
+ +
+ ); +}; + +export { CalendarSwitch }; diff --git a/apps/web/components/getting-started/components/ConnectedCalendarItem.tsx b/apps/web/components/getting-started/components/ConnectedCalendarItem.tsx new file mode 100644 index 0000000000..059844e992 --- /dev/null +++ b/apps/web/components/getting-started/components/ConnectedCalendarItem.tsx @@ -0,0 +1,70 @@ +import { DotsHorizontalIcon } from "@heroicons/react/solid"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +import { CalendarSwitch } from "./CalendarSwitch"; + +interface IConnectedCalendarItem { + name: string; + logo: string; + externalId?: string; + integrationType: string; + calendars?: { + primary: true | null; + isSelected: boolean; + credentialId: number; + name?: string | undefined; + readOnly?: boolean | undefined; + userId?: number | undefined; + integration?: string | undefined; + externalId: string; + }[]; +} + +const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => { + const { name, logo, externalId, calendars, integrationType } = prop; + const { t } = useLocale(); + return ( + <> +
+ {name} +
+

{name}

+
+ + {externalId}{" "} + + + {t("default")} + +
+
+ + +
+
+
+ +
+ + ); +}; + +export { ConnectedCalendarItem }; diff --git a/apps/web/components/getting-started/components/CreateEventsOnCalendarSelect.tsx b/apps/web/components/getting-started/components/CreateEventsOnCalendarSelect.tsx new file mode 100644 index 0000000000..f59ab8f158 --- /dev/null +++ b/apps/web/components/getting-started/components/CreateEventsOnCalendarSelect.tsx @@ -0,0 +1,37 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { inferMutationInput, trpc } from "@calcom/trpc/react"; + +import DestinationCalendarSelector from "@components/DestinationCalendarSelector"; + +interface ICreateEventsOnCalendarSelectProps { + calendar?: inferMutationInput<"viewer.setDestinationCalendar"> | null; +} + +const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps) => { + const { calendar } = props; + const { t } = useLocale(); + const mutation = trpc.useMutation(["viewer.setDestinationCalendar"]); + + return ( + <> +
+
+ +
+ { + mutation.mutate(calendar); + }} + hidePlaceholder + /> +
+
+
+ + ); +}; + +export { CreateEventsOnCalendarSelect }; diff --git a/apps/web/components/getting-started/components/StepCard.tsx b/apps/web/components/getting-started/components/StepCard.tsx new file mode 100644 index 0000000000..42fe2efc33 --- /dev/null +++ b/apps/web/components/getting-started/components/StepCard.tsx @@ -0,0 +1,9 @@ +const StepCard: React.FC<{ children: React.ReactNode }> = (props) => { + return ( +
+ {props.children} +
+ ); +}; + +export { StepCard }; diff --git a/apps/web/components/getting-started/components/Steps.tsx b/apps/web/components/getting-started/components/Steps.tsx new file mode 100644 index 0000000000..084bc54152 --- /dev/null +++ b/apps/web/components/getting-started/components/Steps.tsx @@ -0,0 +1,37 @@ +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +interface ISteps { + maxSteps: number; + currentStep: number; + navigateToStep: (step: number) => void; +} + +const Steps = (props: ISteps) => { + const { maxSteps, currentStep, navigateToStep } = props; + const { t } = useLocale(); + return ( +
+

+ {t("current_step_of_total", { currentStep: currentStep + 1, maxSteps })} +

+
+ {new Array(maxSteps).fill(0).map((_s, index) => { + return index <= currentStep ? ( +
navigateToStep(index)} + className={classNames( + "h-1 w-1/4 bg-black dark:bg-white", + index < currentStep ? "cursor-pointer" : "" + )} + /> + ) : ( +
+ ); + })} +
+
+ ); +}; +export { Steps }; diff --git a/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx b/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx new file mode 100644 index 0000000000..6164624959 --- /dev/null +++ b/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx @@ -0,0 +1,107 @@ +import { ArrowRightIcon } from "@heroicons/react/solid"; + +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { List } from "@calcom/ui/List"; +import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui/v2"; + +import { CalendarItem } from "../components/CalendarItem"; +import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem"; +import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect"; + +interface IConnectCalendarsProps { + nextStep: () => void; +} + +const ConnectedCalendars = (props: IConnectCalendarsProps) => { + const { nextStep } = props; + const queryConnectedCalendars = trpc.useQuery(["viewer.connectedCalendars"]); + const { t } = useLocale(); + const queryIntegrations = trpc.useQuery([ + "viewer.integrations", + { variant: "calendar", onlyInstalled: false }, + ]); + + const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find( + (item) => item.calendars && item.calendars?.length > 0 + ); + const disabledNextButton = firstCalendar === undefined; + const destinationCalendar = queryConnectedCalendars.data?.destinationCalendar; + return ( + <> + {/* Already connected calendars */} + {firstCalendar && + firstCalendar.integration && + firstCalendar.integration.title && + firstCalendar.integration.imageSrc && ( + <> + + 0 + ? firstCalendar.calendars[0].externalId + : "" + } + calendars={firstCalendar.calendars} + integrationType={firstCalendar.integration.type} + /> + + {/* Create event on selected calendar */} + +

{t("connect_calendars_from_app_store")}

+ + )} + + {/* Connect calendars list */} + {firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && ( + + {queryIntegrations.data && + queryIntegrations.data.items.map((item) => ( +
  • + {item.title && item.imageSrc && ( + + )} +
  • + ))} +
    + )} + + {queryConnectedCalendars.isLoading && ( +
      + {[0, 0, 0, 0].map((_item, index) => { + return ( +
    • + + + +
    • + ); + })} +
    + )} + + + ); +}; + +export { ConnectedCalendars }; diff --git a/apps/web/components/getting-started/steps-views/SetupAvailability.tsx b/apps/web/components/getting-started/steps-views/SetupAvailability.tsx new file mode 100644 index 0000000000..dede287c49 --- /dev/null +++ b/apps/web/components/getting-started/steps-views/SetupAvailability.tsx @@ -0,0 +1,93 @@ +import { ArrowRightIcon } from "@heroicons/react/solid"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; + +import { Schedule } from "@calcom/features/schedules"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react"; +import { AppRouter } from "@calcom/trpc/server/routers/_app"; +import { Form } from "@calcom/ui/form/fields"; +import { Button } from "@calcom/ui/v2"; + +import { DEFAULT_SCHEDULE } from "@lib/availability"; +import type { Schedule as ScheduleType } from "@lib/types/schedule"; + +interface ISetupAvailabilityProps { + nextStep: () => void; + defaultScheduleId?: number | null; + defaultAvailability?: { schedule?: TimeRanges[][] }; +} + +interface ScheduleFormValues { + schedule: ScheduleType; +} + +const SetupAvailability = (props: ISetupAvailabilityProps) => { + const { defaultScheduleId } = props; + + const { t } = useLocale(); + const { nextStep } = props; + + const router = useRouter(); + let queryAvailability; + if (defaultScheduleId) { + queryAvailability = trpc.useQuery(["viewer.availability.schedule", { scheduleId: defaultScheduleId }], { + enabled: router.isReady, + }); + } + + const availabilityForm = useForm({ + defaultValues: { schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE }, + }); + + const mutationOptions = { + onError: (error: TRPCClientErrorLike) => { + throw new Error(error.message); + }, + onSuccess: () => { + nextStep(); + }, + }; + const createSchedule = trpc.useMutation("viewer.availability.schedule.create", mutationOptions); + const updateSchedule = trpc.useMutation("viewer.availability.schedule.update", mutationOptions); + return ( + + className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white" + form={availabilityForm} + handleSubmit={async (values) => { + try { + if (defaultScheduleId) { + await updateSchedule.mutate({ + scheduleId: defaultScheduleId, + name: t("default_schedule_name"), + ...values, + }); + } else { + await createSchedule.mutate({ + name: t("default_schedule_name"), + ...values, + }); + } + } catch (error) { + if (error instanceof Error) { + // setError(error); + // @TODO: log error + } + } + }}> + + +
    + +
    + + ); +}; + +export { SetupAvailability }; diff --git a/apps/web/components/getting-started/steps-views/UserProfile.tsx b/apps/web/components/getting-started/steps-views/UserProfile.tsx new file mode 100644 index 0000000000..adcf5fb62a --- /dev/null +++ b/apps/web/components/getting-started/steps-views/UserProfile.tsx @@ -0,0 +1,163 @@ +import { ArrowRightIcon } from "@heroicons/react/solid"; +import { useRouter } from "next/router"; +import { FormEvent, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { User } from "@calcom/prisma/client"; +import { trpc } from "@calcom/trpc/react"; +import { Button, Input } from "@calcom/ui/v2"; + +import { AvatarSSR } from "@components/ui/AvatarSSR"; +import ImageUploader from "@components/v2/settings/ImageUploader"; + +interface IUserProfile { + user?: User; +} + +type FormData = { + bio: string; +}; + +const UserProfile = (props: IUserProfile) => { + const { user } = props; + const { t } = useLocale(); + const avatarRef = useRef(null!); + const bioRef = useRef(null); + const { + register, + setValue, + handleSubmit, + formState: { errors }, + } = useForm({ defaultValues: { bio: user?.bio || "" } }); + const { data: eventTypes } = trpc.useQuery(["viewer.eventTypes.list"]); + const [imageSrc, setImageSrc] = useState(user?.avatar || ""); + const utils = trpc.useContext(); + const router = useRouter(); + const createEventType = trpc.useMutation("viewer.eventTypes.create"); + const onSuccess = async () => { + try { + if (eventTypes?.length === 0) { + await Promise.all( + DEFAULT_EVENT_TYPES.map(async (event) => { + return createEventType.mutate(event); + }) + ); + } + } catch (error) { + console.error(error); + } + + await utils.refetchQueries(["viewer.me"]); + router.push("/"); + }; + const mutation = trpc.useMutation("viewer.updateProfile", { + onSuccess: onSuccess, + }); + const onSubmit = handleSubmit((data) => { + const { bio } = data; + + mutation.mutate({ + bio, + completedOnboarding: true, + }); + }); + + async function updateProfileHandler(event: FormEvent) { + event.preventDefault(); + const enteredAvatar = avatarRef.current.value; + mutation.mutate({ + avatar: enteredAvatar, + }); + } + + const DEFAULT_EVENT_TYPES = [ + { + title: t("15min_meeting"), + slug: "15min", + length: 15, + }, + { + title: t("30min_meeting"), + slug: "30min", + length: 30, + }, + { + title: t("secret_meeting"), + slug: "secret", + length: 15, + hidden: true, + }, + ]; + + return ( +
    +

    {t("profile_picture")}

    +
    + {user && } + +
    + { + avatarRef.current.value = newAvatar; + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + "value" + )?.set; + nativeInputValueSetter?.call(avatarRef.current, newAvatar); + const ev2 = new Event("input", { bubbles: true }); + avatarRef.current.dispatchEvent(ev2); + updateProfileHandler(ev2 as unknown as FormEvent); + setImageSrc(newAvatar); + }} + imageSrc={imageSrc} + /> +
    +
    +
    + + { + setValue("bio", event.target.value); + }} + /> + {errors.bio && ( +

    + {t("required")} +

    + )} +

    + {t("few_sentences_about_yourself")} +

    +
    + +
    + ); +}; + +export default UserProfile; diff --git a/apps/web/components/getting-started/steps-views/UserSettings.tsx b/apps/web/components/getting-started/steps-views/UserSettings.tsx new file mode 100644 index 0000000000..1f6ec0f62d --- /dev/null +++ b/apps/web/components/getting-started/steps-views/UserSettings.tsx @@ -0,0 +1,116 @@ +import { ArrowRightIcon } from "@heroicons/react/outline"; +import { useRef, useState } from "react"; +import { useForm } from "react-hook-form"; + +import dayjs from "@calcom/dayjs"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { User } from "@calcom/prisma/client"; +import { trpc } from "@calcom/trpc/react"; +import TimezoneSelect from "@calcom/ui/form/TimezoneSelect"; +import { Button } from "@calcom/ui/v2"; + +import { UsernameAvailability } from "@components/ui/UsernameAvailability"; + +interface IUserSettingsProps { + user: User; + nextStep: () => void; +} + +type FormData = { + name: string; +}; + +const UserSettings = (props: IUserSettingsProps) => { + const { user, nextStep } = props; + const { t } = useLocale(); + const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess()); + const { register, handleSubmit, formState } = useForm({ + defaultValues: { + name: user?.name || undefined, + }, + reValidateMode: "onChange", + }); + const { errors } = formState; + const defaultOptions = { required: true, maxLength: 255 }; + + const utils = trpc.useContext(); + const onSuccess = async () => { + await utils.invalidateQueries(["viewer.me"]); + nextStep(); + }; + const mutation = trpc.useMutation("viewer.updateProfile", { + onSuccess: onSuccess, + }); + const onSubmit = handleSubmit((data) => { + mutation.mutate({ + name: data.name, + timeZone: selectedTimeZone, + }); + }); + const [currentUsername, setCurrentUsername] = useState(user.username || undefined); + const [inputUsernameValue, setInputUsernameValue] = useState(currentUsername); + const usernameRef = useRef(null!); + + return ( +
    +
    + {/* Username textfield */} + + + {/* Full name textfield */} +
    + + + {errors.name && ( +

    + {t("required")} +

    + )} +
    + {/* Timezone select field */} +
    + + + setSelectedTimeZone(value)} + className="mt-2 w-full rounded-md text-sm" + /> + +

    + {t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString()} +

    +
    +
    + +
    + ); +}; + +export { UserSettings }; diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index c499e0162b..2f51ebe353 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -178,16 +178,16 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { }; return ( - <> +
    -
    +
    - {process.env.NEXT_PUBLIC_WEBSITE_URL}/ + {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
    { autoCapitalize="none" autoCorrect="none" className={classNames( - "mt-0 rounded-l-none", + "mt-0 rounded-md rounded-l-none", markAsError ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0" : "" @@ -317,7 +317,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
    - +
    ); }; diff --git a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx index a2ff34e882..b892881a24 100644 --- a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx @@ -103,16 +103,16 @@ const UsernameTextfield = (props: ICustomUsernameProps) => { }; return ( - <> +
    -
    +
    - {process.env.NEXT_PUBLIC_WEBSITE_URL}/ + {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
    { autoCapitalize="none" autoCorrect="none" className={classNames( - "mt-0 rounded-l-none", + "mt-0 rounded-md rounded-l-none", markAsError ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0" : "" @@ -200,7 +200,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
    - +
    ); }; diff --git a/apps/web/components/ui/form/Select.tsx b/apps/web/components/ui/form/Select.tsx index dc24ed098f..b8be24b811 100644 --- a/apps/web/components/ui/form/Select.tsx +++ b/apps/web/components/ui/form/Select.tsx @@ -80,7 +80,7 @@ function Select< ({ ...theme, - borderRadius: 2, + borderRadius: 6, colors: { ...theme.colors, ...(hasDarkTheme diff --git a/apps/web/pages/apps/installed.tsx b/apps/web/pages/apps/installed.tsx index 81ed03890b..faf0f42286 100644 --- a/apps/web/pages/apps/installed.tsx +++ b/apps/web/pages/apps/installed.tsx @@ -4,6 +4,7 @@ import { InstallAppButton } from "@calcom/app-store/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import type { App } from "@calcom/types/App"; +import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import EmptyScreen from "@calcom/ui/EmptyScreen"; @@ -139,6 +140,30 @@ const IntegrationsContainer = ({ variant, className = "" }: IntegrationsContaine ); }; +// Server side rendering +export async function getServerSideProps(ctx: AppGetServerSidePropsContext) { + // get return-to cookie and redirect if needed + const { cookies } = ctx.req; + if (cookies && cookies["return-to"]) { + const returnTo = cookies["return-to"]; + if (returnTo) { + ctx.res.setHeader( + "Set-Cookie", + "returnToGettingStarted=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ); + return { + redirect: { + destination: `${returnTo}`, + permanent: false, + }, + }; + } + } + return { + props: {}, + }; +} + export default function IntegrationsPage() { const { t } = useLocale(); const query = trpc.useQuery(["viewer.integrations", { onlyInstalled: true }]); diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index dc5d8ea7a2..548072c2f7 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -26,6 +26,10 @@ import { HttpError } from "@lib/core/http/error"; import Schedule from "@components/availability/Schedule"; import EditableHeading from "@components/ui/EditableHeading"; +/** + * @deprecated modifications to this file should be v2 only + * Use `/apps/web/pages/v2/availability/[schedule].tsx` instead + */ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) { const { t } = useLocale(); const router = useRouter(); diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 9dde4122f6..eac426071c 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -16,6 +16,10 @@ import { NewScheduleButton } from "@components/availability/NewScheduleButton"; import { ScheduleListItem } from "@components/availability/ScheduleListItem"; import SkeletonLoader from "@components/availability/SkeletonLoader"; +/** + * @deprecated modifications to this file should be v2 only + * Use `/apps/web/pages/v2/availability/index.tsx` instead + */ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) { const { t } = useLocale(); const utils = trpc.useContext(); diff --git a/apps/web/pages/availability/troubleshoot.tsx b/apps/web/pages/availability/troubleshoot.tsx index 633d5e5d09..7d2394bb79 100644 --- a/apps/web/pages/availability/troubleshoot.tsx +++ b/apps/web/pages/availability/troubleshoot.tsx @@ -1,3 +1,7 @@ +/** + * @deprecated modifications to this file should be v2 only + * Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead + */ import { useState } from "react"; import dayjs from "@calcom/dayjs"; @@ -11,6 +15,10 @@ import Loader from "@components/Loader"; type User = inferQueryOutput<"viewer.me">; +/** + * @deprecated modifications to this file should be v2 only + * Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead + */ const AvailabilityView = ({ user }: { user: User }) => { const { t } = useLocale(); const [selectedDate, setSelectedDate] = useState(dayjs()); diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index 93884eaaa7..d00cffb574 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -811,7 +811,7 @@ const EventTypePage = (props: inferSSRProps) => {
    diff --git a/apps/web/pages/getting-started/[[...step]].tsx b/apps/web/pages/getting-started/[[...step]].tsx new file mode 100644 index 0000000000..f6aeff986b --- /dev/null +++ b/apps/web/pages/getting-started/[[...step]].tsx @@ -0,0 +1,190 @@ +import { GetServerSidePropsContext } from "next"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { z } from "zod"; + +import { getSession } from "@calcom/lib/auth"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { User } from "@calcom/prisma/client"; + +import prisma from "@lib/prisma"; + +import { StepCard } from "@components/getting-started/components/StepCard"; +import { Steps } from "@components/getting-started/components/Steps"; +import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars"; +import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability"; +import UserProfile from "@components/getting-started/steps-views/UserProfile"; +import { UserSettings } from "@components/getting-started/steps-views/UserSettings"; + +interface IOnboardingPageProps { + user: User; +} + +const INITIAL_STEP = "user-settings"; +const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const; + +const stepTransform = (step: typeof steps[number]) => { + const stepIndex = steps.indexOf(step); + if (stepIndex > -1) { + return steps[stepIndex]; + } + return INITIAL_STEP; +}; + +const stepRouteSchema = z.object({ + step: z.array(z.enum(steps)).default([INITIAL_STEP]), +}); + +const OnboardingPage = (props: IOnboardingPageProps) => { + const router = useRouter(); + + const { user } = props; + const { t } = useLocale(); + + const result = stepRouteSchema.safeParse(router.query); + const currentStep = result.success ? result.data.step[0] : INITIAL_STEP; + + const headers = [ + { + title: `${t("welcome_to_calcom")}!`, + subtitle: [`${t("we_just_need_basic_info")}`], + skipText: `${t("skip")}`, + }, + { + title: `${t("connect_your_calendar")}`, + subtitle: [`${t("connect_your_calendar_instructions")}`], + skipText: `${t("do_this_later")}`, + }, + { + title: `${t("set_availability")}`, + subtitle: [ + `${t("set_availability_getting_started_subtitle_1")}`, + `${t("set_availability_getting_started_subtitle_2")}`, + ], + skipText: `${t("do_this_later")}`, + }, + { + title: `${t("nearly_there")}`, + subtitle: [`${t("nearly_there_instructions")}`], + }, + ]; + + const goToIndex = (index: number) => { + const newStep = steps[index]; + router.push( + { + pathname: `/getting-started/${stepTransform(newStep)}`, + }, + undefined + ); + }; + + const currentStepIndex = steps.indexOf(currentStep); + + return ( +
    + + Cal.com - {t("getting_started")} + + + +
    +
    +
    +
    +
    +

    + {headers[currentStepIndex]?.title || "Undefined title"} +

    + + {headers[currentStepIndex]?.subtitle.map((subtitle, index) => ( +

    + {subtitle} +

    + ))} +
    + +
    + + {currentStep === "user-settings" && goToIndex(1)} />} + + {currentStep === "connected-calendar" && goToIndex(2)} />} + + {currentStep === "setup-availability" && ( + goToIndex(3)} defaultScheduleId={user.defaultScheduleId} /> + )} + + {currentStep === "user-profile" && } + + {headers[currentStepIndex]?.skipText && ( + + )} +
    +
    +
    +
    + ); +}; + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const crypto = await import("crypto"); + const session = await getSession(context); + + if (!session?.user?.id) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } + + const user = await prisma.user.findUnique({ + where: { + id: session.user.id, + }, + select: { + id: true, + username: true, + name: true, + email: true, + bio: true, + avatar: true, + timeZone: true, + weekStart: true, + hideBranding: true, + theme: true, + plan: true, + brandColor: true, + darkBrandColor: true, + metadata: true, + timeFormat: true, + allowDynamicBooking: true, + defaultScheduleId: true, + completedOnboarding: true, + }, + }); + + if (!user) { + throw new Error("User from session not found"); + } + + return { + props: { + user: { + ...user, + emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), + }, + }, + }; +}; + +export default OnboardingPage; diff --git a/apps/web/pages/getting-started.tsx b/apps/web/pages/old-getting-started.tsx similarity index 99% rename from apps/web/pages/getting-started.tsx rename to apps/web/pages/old-getting-started.tsx index aa31e6a502..cd14bbfa79 100644 --- a/apps/web/pages/getting-started.tsx +++ b/apps/web/pages/old-getting-started.tsx @@ -1,3 +1,4 @@ +// @@DEPRECATED, use new getting-started.tsx instead import { zodResolver } from "@hookform/resolvers/zod"; import { Prisma } from "@prisma/client"; import classnames from "classnames"; @@ -43,9 +44,9 @@ import { TRPCClientErrorLike } from "@trpc/client"; // Embed isn't applicable to onboarding, so ignore the rule /* eslint-disable @calcom/eslint/avoid-web-storage */ -type ScheduleFormValues = { +export interface ScheduleFormValues { schedule: ScheduleType; -}; +} let mutationComplete: ((err: Error | null) => void) | null; diff --git a/apps/web/pages/v2/availability/[schedule].tsx b/apps/web/pages/v2/availability/[schedule].tsx index b5b8437e3b..0c887071a4 100644 --- a/apps/web/pages/v2/availability/[schedule].tsx +++ b/apps/web/pages/v2/availability/[schedule].tsx @@ -4,14 +4,15 @@ import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; -import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability"; +import { Schedule } from "@calcom/features/schedules"; +import { availabilityAsString, DEFAULT_SCHEDULE } from "@calcom/lib/availability"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; import { inferQueryOutput, trpc } from "@calcom/trpc/react"; import { BadgeCheckIcon } from "@calcom/ui/Icon"; import Shell from "@calcom/ui/Shell"; import TimezoneSelect from "@calcom/ui/form/TimezoneSelect"; -import { Button, Switch, Schedule, Form, TextField, showToast } from "@calcom/ui/v2"; +import { Button, Form, showToast, Switch, TextField } from "@calcom/ui/v2"; import { QueryCell } from "@lib/QueryCell"; import { HttpError } from "@lib/core/http/error"; @@ -62,9 +63,9 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.sc }} className="-mx-5 flex flex-col sm:mx-0 xl:flex-row">
    -
    +

    {t("change_start_end")}

    - +
    +
    +
    + ); +}; + +export default Schedule; diff --git a/packages/ui/v2/modules/availability/ScheduleListItem.tsx b/packages/features/schedules/components/ScheduleListItem.tsx similarity index 100% rename from packages/ui/v2/modules/availability/ScheduleListItem.tsx rename to packages/features/schedules/components/ScheduleListItem.tsx diff --git a/packages/features/schedules/components/index.ts b/packages/features/schedules/components/index.ts new file mode 100644 index 0000000000..a6df3bef22 --- /dev/null +++ b/packages/features/schedules/components/index.ts @@ -0,0 +1,3 @@ +export { NewScheduleButton } from "./NewScheduleButton"; +export { default as Schedule } from "./Schedule"; +export { ScheduleListItem } from "./ScheduleListItem"; diff --git a/packages/features/schedules/index.ts b/packages/features/schedules/index.ts new file mode 100644 index 0000000000..40b494c5f8 --- /dev/null +++ b/packages/features/schedules/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/packages/features/users/components/UserV2OptInBanner.tsx b/packages/features/users/components/UserV2OptInBanner.tsx index 46daf21273..2ee61cb85c 100644 --- a/packages/features/users/components/UserV2OptInBanner.tsx +++ b/packages/features/users/components/UserV2OptInBanner.tsx @@ -20,7 +20,6 @@ function UserV2OptInBanner() { . } - className="mb-2" /> ); @@ -36,7 +35,6 @@ function UserV2OptInBanner() { . } - className="mb-2" /> ); } diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index a1e189d268..f400306df0 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -600,8 +600,8 @@ const loggedInViewerRouter = createProtectedRouter() input: z.object({ integration: z.string(), externalId: z.string(), - eventTypeId: z.number().optional(), - bookingId: z.number().optional(), + eventTypeId: z.number().nullish(), + bookingId: z.number().nullish(), }), async resolve({ ctx, input }) { const { user } = ctx; diff --git a/packages/trpc/server/routers/viewer/availability.tsx b/packages/trpc/server/routers/viewer/availability.tsx index df6474f79b..6822a1be5e 100644 --- a/packages/trpc/server/routers/viewer/availability.tsx +++ b/packages/trpc/server/routers/viewer/availability.tsx @@ -1,8 +1,9 @@ -import { Prisma } from "@prisma/client"; +import { Availability as AvailabilityModel, Prisma, Schedule as ScheduleModel, User } from "@prisma/client"; import { z } from "zod"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; import { getAvailabilityFromSchedule } from "@calcom/lib/availability"; +import { PrismaClient } from "@calcom/prisma/client"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; import { Schedule } from "@calcom/types/schedule"; @@ -66,34 +67,7 @@ export const availabilityRouter = createProtectedRouter() code: "UNAUTHORIZED", }); } - const availability = schedule.availability.reduce( - (schedule: Schedule, availability) => { - availability.days.forEach((day) => { - schedule[day].push({ - start: new Date( - Date.UTC( - new Date().getUTCFullYear(), - new Date().getUTCMonth(), - new Date().getUTCDate(), - availability.startTime.getUTCHours(), - availability.startTime.getUTCMinutes() - ) - ), - end: new Date( - Date.UTC( - new Date().getUTCFullYear(), - new Date().getUTCMonth(), - new Date().getUTCDate(), - availability.endTime.getUTCHours(), - availability.endTime.getUTCMinutes() - ) - ), - }); - }); - return schedule; - }, - Array.from([...Array(7)]).map(() => []) - ); + const availability = convertScheduleToAvailability(schedule); return { schedule, availability, @@ -160,6 +134,12 @@ export const availabilityRouter = createProtectedRouter() const schedule = await prisma.schedule.create({ data, }); + const hasDefaultScheduleId = await hasDefaultSchedule(user, prisma); + + if (hasDefaultScheduleId) { + await setupDefaultSchedule(user.id, schedule.id, prisma); + } + return { schedule }; }, }) @@ -219,14 +199,7 @@ export const availabilityRouter = createProtectedRouter() const availability = getAvailabilityFromSchedule(input.schedule); if (input.isDefault) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - defaultScheduleId: input.scheduleId, - }, - }); + setupDefaultSchedule(user.id, input.scheduleId, prisma); } // Not able to update the schedule with userId where clause, so fetch schedule separately and then validate @@ -277,3 +250,60 @@ export const availabilityRouter = createProtectedRouter() }; }, }); + +export const convertScheduleToAvailability = ( + schedule: Partial & { availability: AvailabilityModel[] } +) => { + return schedule.availability.reduce( + (schedule: Schedule, availability) => { + availability.days.forEach((day) => { + schedule[day].push({ + start: new Date( + Date.UTC( + new Date().getUTCFullYear(), + new Date().getUTCMonth(), + new Date().getUTCDate(), + availability.startTime.getUTCHours(), + availability.startTime.getUTCMinutes() + ) + ), + end: new Date( + Date.UTC( + new Date().getUTCFullYear(), + new Date().getUTCMonth(), + new Date().getUTCDate(), + availability.endTime.getUTCHours(), + availability.endTime.getUTCMinutes() + ) + ), + }); + }); + return schedule; + }, + Array.from([...Array(7)]).map(() => []) + ); +}; + +const setupDefaultSchedule = async (userId: number, scheduleId: number, prisma: PrismaClient) => { + await prisma.user.update({ + where: { + id: userId, + }, + data: { + defaultScheduleId: scheduleId, + }, + }); +}; + +const isDefaultSchedule = (scheduleId: number, user: Partial) => { + return !user.defaultScheduleId || user.defaultScheduleId === scheduleId; +}; + +const hasDefaultSchedule = async (user: Partial, prisma: PrismaClient) => { + const defaultSchedule = await prisma.schedule.findFirst({ + where: { + userId: user.id, + }, + }); + return !!user.defaultScheduleId || !!defaultSchedule; +}; diff --git a/packages/ui/form/TimezoneSelect.tsx b/packages/ui/form/TimezoneSelect.tsx index e3c2dd0d02..aa2f5f767b 100644 --- a/packages/ui/form/TimezoneSelect.tsx +++ b/packages/ui/form/TimezoneSelect.tsx @@ -10,15 +10,18 @@ import BaseSelect, { import { InputComponent } from "@calcom/ui/v2/core/form/Select"; function TimezoneSelect({ className, ...props }: SelectProps) { + // @TODO: remove borderRadius and haveRoundedClassName logic from theme so we use only new style + const haveRoundedClassName = !!(className && className.indexOf("rounded-") > -1); + const defaultBorderRadius = 2; + return ( ({ ...theme, - borderRadius: 2, + ...(haveRoundedClassName ? {} : { borderRadius: defaultBorderRadius }), colors: { ...theme.colors, primary: "var(--brand-color)", - primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))", primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))", }, diff --git a/packages/ui/v2/core/Switch.tsx b/packages/ui/v2/core/Switch.tsx index 9bab6a2a3e..6685221337 100644 --- a/packages/ui/v2/core/Switch.tsx +++ b/packages/ui/v2/core/Switch.tsx @@ -17,11 +17,11 @@ const Switch = ( const id = useId(); return ( -
    +
    @@ -30,7 +30,9 @@ const Switch = ( // Since we dont support global dark mode - we have to style dark mode components specifically on the instance for now // TODO: Remove once we support global dark mode className={classNames( - "block h-[18px] w-[18px] translate-x-0 rounded-full bg-white transition-transform", + "block h-[18px] w-[18px] rounded-full bg-white", + "translate-x-[4px] transition delay-100 will-change-transform", + "[&[data-state='checked']]:translate-x-[18px]", props.checked && "shadow-inner", props.thumbProps?.className )} diff --git a/packages/ui/v2/core/form/Select.tsx b/packages/ui/v2/core/form/Select.tsx index b438cabc51..9a0e5ad12e 100644 --- a/packages/ui/v2/core/form/Select.tsx +++ b/packages/ui/v2/core/form/Select.tsx @@ -49,6 +49,14 @@ function Select< }, })} styles={{ + control: (base) => ({ + ...base, + // Brute force to remove focus outline of input + "& .react-select__input": { + borderWidth: 0, + boxShadow: "none", + }, + }), option: (provided, state) => ({ ...provided, backgroundColor: state.isSelected ? "var(--brand-color)" : state.isFocused ? "#F3F4F6" : "", @@ -63,6 +71,7 @@ function Select< ...components, IndicatorSeparator: () => null, Input: InputComponent, + ...props.components, }} {...props} /> diff --git a/packages/ui/v2/modules/availability/Schedule.tsx b/packages/ui/v2/modules/availability/Schedule.tsx deleted file mode 100644 index 1f67a2b46b..0000000000 --- a/packages/ui/v2/modules/availability/Schedule.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import classNames from "classnames"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import { GroupBase, Props } from "react-select"; - -import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs"; -import { defaultDayRange } from "@calcom/lib/availability"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { weekdayNames } from "@calcom/lib/weekday"; -import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; -import { TimeRange } from "@calcom/types/schedule"; -import Dropdown, { DropdownMenuContent } from "@calcom/ui/Dropdown"; -import { Icon } from "@calcom/ui/Icon"; -import Button from "@calcom/ui/v2/core/Button"; -import Switch from "@calcom/ui/v2/core/Switch"; -import Tooltip from "@calcom/ui/v2/core/Tooltip"; -import Select from "@calcom/ui/v2/core/form/Select"; - -/** Begin Time Increments For Select */ -const increment = 15; - -type Option = { - readonly label: string; - readonly value: number; -}; - -/** - * Creates an array of times on a 15 minute interval from - * 00:00:00 (Start of day) to - * 23:45:00 (End of day with enough time for 15 min booking) - */ -const useOptions = () => { - // Get user so we can determine 12/24 hour format preferences - const query = useMeQuery(); - const { timeFormat } = query.data || { timeFormat: null }; - - const [filteredOptions, setFilteredOptions] = useState([]); - - const options = useMemo(() => { - const end = dayjs().utc().endOf("day"); - let t: Dayjs = dayjs().utc().startOf("day"); - - const options: Option[] = []; - while (t.isBefore(end)) { - options.push({ - value: t.toDate().valueOf(), - label: dayjs(t) - .utc() - .format(timeFormat === 12 ? "h:mma" : "HH:mm"), - }); - t = t.add(increment, "minutes"); - } - return options; - }, [timeFormat]); - - const filter = useCallback( - ({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => { - if (current) { - const currentOption = options.find((option) => option.value === dayjs(current).toDate().valueOf()); - if (currentOption) setFilteredOptions([currentOption]); - } else - setFilteredOptions( - options.filter((option) => { - const time = dayjs(option.value); - return (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset)); - }) - ); - }, - [options] - ); - - return { options: filteredOptions, filter }; -}; - -type TimeRangeFieldProps = { - name: string; - className?: string; -}; - -const LazySelect = ({ - value, - min, - max, - ...props -}: Omit>, "value"> & { - value: ConfigType; - min?: ConfigType; - max?: ConfigType; -}) => { - // Lazy-loaded options, otherwise adding a field has a noticable redraw delay. - const { options, filter } = useOptions(); - - useEffect(() => { - filter({ current: value }); - }, [filter, value]); - - return ( - { - if (e.target.checked && !selected.includes(num)) { - setSelected(selected.concat([num])); - } else if (!e.target.checked && selected.includes(num)) { - setSelected(selected.slice(selected.indexOf(num), 1)); - } - }} - type="checkbox" - className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400" - /> - - - ))} - -
    - -
    -
    - ); -}; - -export const DayRanges = ({ - name, - defaultValue = [defaultDayRange], -}: { - name: string; - defaultValue?: TimeRange[]; -}) => { - const { setValue, watch } = useFormContext(); - // XXX: Hack to make copying times work; `fields` is out of date until save. - const watcher = watch(name); - const { t } = useLocale(); - const { fields, replace, append, remove } = useFieldArray({ - name, - }); - - useEffect(() => { - if (defaultValue.length && !fields.length) { - replace(defaultValue); - } - }, [replace, defaultValue, fields.length]); - - const handleAppend = () => { - // FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499 - const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end); - const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour"); - - if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) { - return append({ - start: nextRangeStart.toDate(), - end: nextRangeEnd.toDate(), - }); - } - }; - - return ( -
    - {fields.map((field, index) => ( -
    -
    - -
    - {index === 0 && ( -
    - -
    - )} -
    - ))} -
    - ); -}; - -const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { - const { t } = useLocale(); - - const form = useFormContext(); - const watchAvailable = form.watch(`${name}.${day}`, []); - - return ( -
    - - {!!watchAvailable.length && ( -
    - -
    - )} -
    - ); -}; - -const Schedule = ({ name }: { name: string }) => { - const { i18n } = useLocale(); - return ( -
    - {weekdayNames(i18n.language).map((weekday, num) => ( - - ))} -
    - ); -}; - -export default Schedule; diff --git a/packages/ui/v2/modules/availability/index.ts b/packages/ui/v2/modules/availability/index.ts deleted file mode 100644 index 5f8114de52..0000000000 --- a/packages/ui/v2/modules/availability/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./NewScheduleButton"; -export { default as Schedule } from "./Schedule"; -export * from "./ScheduleListItem"; diff --git a/packages/ui/v2/modules/index.ts b/packages/ui/v2/modules/index.ts index ce87e722b0..e5c8acf913 100644 --- a/packages/ui/v2/modules/index.ts +++ b/packages/ui/v2/modules/index.ts @@ -1,4 +1,3 @@ export * from "./auth"; -export * from "./availability"; export * from "./booker"; export * from "./event-types";