diff --git a/packages/atoms/availability-list/components/availability/index.tsx b/packages/atoms/availability-list/components/availability/index.tsx new file mode 100644 index 0000000000..c8465ed40e --- /dev/null +++ b/packages/atoms/availability-list/components/availability/index.tsx @@ -0,0 +1,120 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { Toaster } from "@/components/ui/toaster"; +import { useToast } from "@/components/ui/use-toast"; +import type { Schedule } from "availability-list"; +import { Globe, MoreHorizontal, Star, Copy, Trash } from "lucide-react"; +import { Fragment } from "react"; + +import { availabilityAsString } from "@calcom/lib/availability"; + +type AvailabilityProps = { + schedule: Schedule; + isDeletable: boolean; + updateDefault: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void; + duplicateFunction: ({ scheduleId }: { scheduleId: number }) => void; + deleteFunction: ({ scheduleId }: { scheduleId: number }) => void; + displayOptions?: { + timeZone?: string; + hour12?: boolean; + }; +}; + +export function Availability({ + schedule, + isDeletable, + displayOptions, + updateDefault, + duplicateFunction, + deleteFunction, +}: AvailabilityProps) { + const { toast } = useToast(); + + return ( +
  • +
    +
    + +

    {schedule.name}

    +
    + {schedule.isDefault && Default} +
    +

    + {schedule.availability + .filter((availability) => !!availability.days.length) + .map((availability) => ( + + {availabilityAsString(availability, { + hour12: displayOptions?.hour12, + })} +
    +
    + ))} + {(schedule.timeZone || displayOptions?.timeZone) && ( +

    + +  {schedule.timeZone ?? displayOptions?.timeZone} +

    + )} +

    +
    +
    + + + + + + {!schedule.isDefault && ( + { + updateDefault({ + scheduleId: schedule.id, + isDefault: true, + }); + }} + className="min-w-40 focus:ring-mute min-w-40 focus:ring-muted"> + + Set as default + + )} + { + duplicateFunction({ + scheduleId: schedule.id, + }); + }}> + + Duplicate + + { + if (!isDeletable) { + toast({ + description: "You are required to have at least one schedule", + }); + } else { + deleteFunction({ + scheduleId: schedule.id, + }); + } + }}> + + Delete + + + + +
    +
  • + ); +} diff --git a/packages/atoms/availability-list/components/empty-screen/index.tsx b/packages/atoms/availability-list/components/empty-screen/index.tsx new file mode 100644 index 0000000000..6cf320e20d --- /dev/null +++ b/packages/atoms/availability-list/components/empty-screen/index.tsx @@ -0,0 +1,70 @@ +import { Button } from "@/components/ui/button"; +import type { LucideIcon as IconType } from "lucide-react"; +import type { ReactNode } from "react"; +import React from "react"; + +import { classNames } from "@calcom/lib"; +import type { SVGComponent } from "@calcom/types/SVGComponent"; + +type EmptyScreenProps = { + Icon?: SVGComponent | IconType; + avatar?: React.ReactElement; + headline: string | React.ReactElement; + description?: string | React.ReactElement; + buttonText?: string; + buttonOnClick?: (event: React.MouseEvent) => void; + buttonRaw?: ReactNode; // Used incase you want to provide your own button. + border?: boolean; + dashedBorder?: boolean; +}; + +export function EmptyScreen({ + Icon, + avatar, + headline, + description, + buttonText, + buttonOnClick, + buttonRaw, + border = true, + dashedBorder = true, + className, +}: EmptyScreenProps & React.HTMLAttributes) { + return ( + <> +
    + {!avatar ? null : ( +
    {avatar}
    + )} + {!Icon ? null : ( +
    + +
    + )} +
    +

    + {headline} +

    + {description && ( +
    + {description} +
    + )} + {buttonOnClick && buttonText && } + {buttonRaw} +
    +
    + + ); +} diff --git a/packages/atoms/availability-list/components/form/index.tsx b/packages/atoms/availability-list/components/form/index.tsx new file mode 100644 index 0000000000..fd72a3c99e --- /dev/null +++ b/packages/atoms/availability-list/components/form/index.tsx @@ -0,0 +1,39 @@ +import type { ReactElement, Ref } from "react"; +import React, { forwardRef } from "react"; +import type { FieldValues, SubmitHandler, UseFormReturn } from "react-hook-form"; +import { FormProvider } from "react-hook-form"; + +type FormProps = { form: UseFormReturn; handleSubmit: SubmitHandler } & Omit< + JSX.IntrinsicElements["form"], + "onSubmit" +>; + +const PlainForm = (props: FormProps, ref: Ref) => { + const { form, handleSubmit, ...passThrough } = props; + + return ( + +
    { + event.preventDefault(); + event.stopPropagation(); + + form + .handleSubmit(handleSubmit)(event) + .catch((err) => { + // FIXME: Booking Pages don't have toast, so this error is never shown + // showToast(`${getErrorFromUnknown(err).message}`, "error"); + console.error(`${getErrorFromUnknown(err).message}`, "error"); + }); + }} + {...passThrough}> + {props.children} +
    +
    + ); +}; + +export const Form = forwardRef(PlainForm) as ( + p: FormProps & { ref?: Ref } +) => ReactElement; diff --git a/packages/atoms/availability-list/components/new-schedule-button/index.tsx b/packages/atoms/availability-list/components/new-schedule-button/index.tsx new file mode 100644 index 0000000000..7494b15971 --- /dev/null +++ b/packages/atoms/availability-list/components/new-schedule-button/index.tsx @@ -0,0 +1,68 @@ +import { + Dialog, + DialogTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useForm } from "react-hook-form"; + +import type { HttpError } from "@calcom/lib/http-error"; +import { Plus } from "@calcom/ui/components/icon"; + +import { Button } from "../../../src/components/ui/button"; +import { Form } from "../form"; +import type { Schedule } from ".prisma/client"; + +// create mutation handler to be handled outside the component +// then passed in as a prop +// TODO: translations can be taken care of later + +type NewScheduleButtonProps = { + name?: string; + createMutation: (values: { + onSucess: (schedule: Schedule) => void; + onError: (err: HttpError) => void; + }) => void; +}; + +export function NewScheduleButton({ name = "new-schedule", createMutation }: NewScheduleButtonProps) { + const form = useForm<{ + name: string; + }>(); + + return ( +
    + + + + + + + Add new schedule +
    { + createMutation(values); + }}> + + + + + + +
    +
    +
    +
    +
    + ); +} diff --git a/packages/atoms/availability-list/export.ts b/packages/atoms/availability-list/export.ts new file mode 100644 index 0000000000..cf8a1d71d4 --- /dev/null +++ b/packages/atoms/availability-list/export.ts @@ -0,0 +1,3 @@ +export { AvailabilityList } from "."; +export { Availability } from "./components/availability"; +export * from "../types"; diff --git a/packages/atoms/availability-list/index.tsx b/packages/atoms/availability-list/index.tsx new file mode 100644 index 0000000000..3c71f07dc1 --- /dev/null +++ b/packages/atoms/availability-list/index.tsx @@ -0,0 +1,74 @@ +import { NewScheduleButton } from "availability-list/components/new-schedule-button/NewScheduleButton"; + +import type { HttpError } from "@calcom/lib/http-error"; +import { Clock } from "@calcom/ui/components/icon"; + +import { Availability } from "./components/availability"; +import { EmptyScreen } from "./components/empty-screen"; + +export type Schedule = { + isDefault: boolean; + id: number; + name: string; + availability: { + id: number; + startTime: Date; + endTime: Date; + userId?: number; + eventTypeId?: number; + date?: Date; + days: number[]; + scheduleId?: number; + }[]; + timezone?: string; +}; + +type AvailabilityListProps = { + schedules: Schedule[] | []; + onCreateMutation: (values: { + onSucess: (schedule: Schedule) => void; + onError: (err: HttpError) => void; + }) => void; + updateMutation: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void; + duplicateMutation: ({ scheduleId }: { scheduleId: number }) => void; + deleteMutation: ({ scheduleId }: { scheduleId: number }) => void; +}; + +export function AvailabilityList({ + schedules, + onCreateMutation, + updateMutation, + duplicateMutation, + deleteMutation, +}: AvailabilityListProps) { + if (schedules.length === 0) { + return ( +
    + } + /> +
    + ); + } + + return ( +
    +
      + {schedules.map((schedule) => ( + + ))} +
    +
    + ); +}