restructure folder

availability-list
Ryukemeister 2023-10-30 18:41:56 +05:30
parent fa796f07d5
commit c7aac9bbc2
6 changed files with 374 additions and 0 deletions

View File

@ -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 (
<li key={schedule.id}>
<div className="hover:bg-muted flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="group flex w-full items-center justify-between sm:px-6">
<a className="flex-grow truncate text-sm" href={`/availability/${schedule.id}`}>
<h1>{schedule.name}</h1>
<div className="space-x-2 rtl:space-x-reverse">
{schedule.isDefault && <Badge className="bg-success text-success text-xs">Default</Badge>}
</div>
<p className="text-subtle mt-1">
{schedule.availability
.filter((availability) => !!availability.days.length)
.map((availability) => (
<Fragment key={availability.id}>
{availabilityAsString(availability, {
hour12: displayOptions?.hour12,
})}
<br />
</Fragment>
))}
{(schedule.timeZone || displayOptions?.timeZone) && (
<p className="my-1 flex items-center first-letter:text-xs">
<Globe className="h-3.5 w-3.5" />
&nbsp;{schedule.timeZone ?? displayOptions?.timeZone}
</p>
)}
</p>
</a>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<Button type="button" className="mx-5" color="secondary">
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!schedule.isDefault && (
<DropdownMenuItem
onClick={() => {
updateDefault({
scheduleId: schedule.id,
isDefault: true,
});
}}
className="min-w-40 focus:ring-mute min-w-40 focus:ring-muted">
<Star />
Set as default
</DropdownMenuItem>
)}
<DropdownMenuItem
className="outline-none"
onClick={() => {
duplicateFunction({
scheduleId: schedule.id,
});
}}>
<Copy />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="min-w-40 focus:ring-muted"
onClick={() => {
if (!isDeletable) {
toast({
description: "You are required to have at least one schedule",
});
} else {
deleteFunction({
scheduleId: schedule.id,
});
}
}}>
<Trash />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Toaster />
</div>
</li>
);
}

View File

@ -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<HTMLElement, 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<HTMLDivElement>) {
return (
<>
<div
data-testid="empty-screen"
className={classNames(
"flex w-full select-none flex-col items-center justify-center rounded-lg p-7 lg:p-20",
border && "border-subtle border",
dashedBorder && "border-dashed",
className
)}>
{!avatar ? null : (
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full">{avatar}</div>
)}
{!Icon ? null : (
<div className="bg-emphasis flex h-[72px] w-[72px] items-center justify-center rounded-full ">
<Icon className="text-default inline-block h-10 w-10 stroke-[1.3px]" />
</div>
)}
<div className="flex max-w-[420px] flex-col items-center">
<h2
className={classNames(
"text-semibold font-cal text-emphasis text-center text-xl",
Icon && "mt-6"
)}>
{headline}
</h2>
{description && (
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
{description}
</div>
)}
{buttonOnClick && buttonText && <Button onClick={(e) => buttonOnClick(e)}>{buttonText}</Button>}
{buttonRaw}
</div>
</div>
</>
);
}

View File

@ -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<T extends object> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return (
<FormProvider {...form}>
<form
ref={ref}
onSubmit={(event) => {
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}
</form>
</FormProvider>
);
};
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;

View File

@ -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 (
<div>
<Dialog>
<DialogTrigger asChild>
<Button type="button" data-testid={name}>
{Plus}
New
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add new schedule</DialogTitle>
<Form
form={form}
handleSubmit={(values) => {
createMutation(values);
}}>
<Label htmlFor="working-hours">Name</Label>
<Input id="working-hours" placeholder="Working Hours" />
<DialogFooter>
<Button type="button" variant="outline" className="mr-2 border-none">
Close
</Button>
<Button type="submit">Continue</Button>
</DialogFooter>
</Form>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,3 @@
export { AvailabilityList } from ".";
export { Availability } from "./components/availability";
export * from "../types";

View File

@ -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 (
<div className="flex justify-center">
<EmptyScreen
Icon={Clock}
headline="Create an availability schedule"
subtitle="Creating availability schedules allows you to manage availability across event types. They can be applied to one or more event types."
className="w-full"
buttonRaw={<NewScheduleButton createMutation={onCreateMutation} />}
/>
</div>
);
}
return (
<div className="border-subtle bg-default mb-16 overflow-hidden rounded-md border">
<ul className="divide-subtle divide-y" data-testid="schedules">
{schedules.map((schedule) => (
<Availability
key={schedule.id}
schedule={schedule}
isDeletable={schedules.length !== 1}
updateDefault={updateMutation}
deleteFunction={deleteMutation}
duplicateFunction={duplicateMutation}
/>
))}
</ul>
</div>
);
}