Compare commits

...

45 Commits

Author SHA1 Message Date
Ryukemeister 730f9267d2 minor fixes 2023-11-01 01:00:35 +05:30
Ryukemeister f2980eb6c6 Merge branch 'main' into availability-list 2023-10-30 22:58:15 +05:30
Ryukemeister 49725e11b3 replace dropdown with controls component 2023-10-30 22:56:58 +05:30
Ryukemeister 508a5db62f add controls component for dropdown and toaster 2023-10-30 22:55:39 +05:30
Ryukemeister a1e7d16822 update Schedule types 2023-10-30 19:06:41 +05:30
Ryukemeister 0ff0c1203b fix typo in prop name 2023-10-30 18:59:10 +05:30
Ryukemeister 344c2c3996 update export paths 2023-10-30 18:42:25 +05:30
Ryukemeister c7aac9bbc2 restructure folder 2023-10-30 18:41:56 +05:30
Ryukemeister fa796f07d5 restructure folders 2023-10-30 18:41:09 +05:30
Ryukemeister d4156f5dc7 create separate types for each component prop 2023-10-28 00:21:01 +05:30
Ryukemeister 97921ea035 availability component 2023-10-28 00:11:48 +05:30
Ryukemeister 11a78f95bd renamae availability component file 2023-10-28 00:10:55 +05:30
Ryukemeister 79dba1827c cleanup comments 2023-10-27 23:27:26 +05:30
Ryukemeister 0d4f36c00f code separation and fix typings 2023-10-27 22:49:01 +05:30
Ryukemeister 2e8398a255 remove type schedule and import it from availability list 2023-10-27 22:48:11 +05:30
Ryukemeister 7723cbfee7 add AvailabilityList components to default export 2023-10-16 17:43:41 +05:30
Ryukemeister 0b2c730c72 add standalone availability component to exports 2023-10-16 16:50:35 +05:30
Ryukemeister 501f9ad2fd update component names 2023-10-16 16:49:48 +05:30
Ryukemeister 6d514a27d7 rename function name 2023-10-16 16:48:49 +05:30
Ryukemeister b6030c321a update packages 2023-10-16 15:22:54 +05:30
Ryukemeister 4b42404f4a add dropdown, toaster and remaining components 2023-10-16 15:22:20 +05:30
Ryukemeister 9d3b512ef6 update props and add schedule list item to view 2023-10-16 15:21:32 +05:30
Ryukemeister 7a4f1fc098 add dropdown and toaster components from shadcn 2023-10-16 15:20:33 +05:30
Ryukemeister 7329531839 update list vview 2023-10-12 18:20:10 +05:30
Ryukemeister 0cbac3b01d cleanup 2023-10-12 18:18:22 +05:30
Ryukemeister fef821022e add schedule item for each day 2023-10-12 18:17:46 +05:30
Ryukemeister 0f0b879ebe add badge component 2023-10-12 18:16:33 +05:30
Ryukemeister 18318f8501 update styles for close button 2023-10-11 18:32:25 +05:30
Ryukemeister 38ac778fbc update typings 2023-10-11 18:31:28 +05:30
Ryukemeister 11dde215bc update view for new schedule button 2023-10-11 18:30:54 +05:30
Ryukemeister de0bd85ea5 add input and label for form component 2023-10-11 18:30:06 +05:30
Ryukemeister 2273a510f3 update packages 2023-10-11 18:29:27 +05:30
Ryukemeister e62bfc1825 add dialog for new schedule 2023-10-10 22:36:24 +05:30
Ryukemeister 4a7a9e391e update types for schedules 2023-10-10 22:35:51 +05:30
Ryukemeister 0f6f5c716c setup dialog from shadcn 2023-10-10 22:35:03 +05:30
Ryukemeister 8ba3a79f0e add react and react hook for 2023-10-10 14:47:53 +05:30
Ryukemeister 7adfe559ba update list view 2023-10-10 14:47:17 +05:30
Ryukemeister 5e9394248e empty screen dialog 2023-10-10 14:46:34 +05:30
Ryukemeister e948c5cfb8 helpers for empty screen 2023-10-10 14:20:59 +05:30
Ryukemeister e4a5062008 setup 2023-10-09 21:50:29 +05:30
Ryukemeister 6f14fe7f4e add btn from shadcn ui 2023-10-09 21:47:53 +05:30
Ryukemeister 743a80f2b2 shadcn setup 2023-10-09 21:47:01 +05:30
Ryukemeister 0b90f1b698 config for shadcn 2023-10-09 21:46:15 +05:30
Ryukemeister 95bd0ef83f setting up shadcn 2023-10-09 21:45:33 +05:30
Ryukemeister de218e96bb init availabilitylist component 2023-10-09 21:31:03 +05:30
24 changed files with 1317 additions and 4 deletions

View File

@ -0,0 +1,95 @@
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast";
import type { Schedule } from "availability-list";
import { Controls } from "availability-list/components/controls";
import { Globe } 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();
function handleDelete() {
if (!isDeletable) {
toast({
description: "You are required to have at least one schedule",
});
} else {
deleteFunction({
scheduleId: schedule.id,
});
}
}
function handleSetDefault() {
updateDefault({
scheduleId: schedule.id,
isDefault: true,
});
}
function handleDuplicate() {
duplicateFunction({
scheduleId: schedule.id,
});
}
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>
<Controls
schedule={schedule}
handleDelete={handleDelete}
handleDuplicate={handleDuplicate}
handleSetDefault={handleSetDefault}
/>
</div>
</li>
);
}

View File

@ -0,0 +1,60 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Toaster } from "@/components/ui/toaster";
import type { Schedule } from "availability-list";
import { MoreHorizontal, Star, Copy, Trash } from "lucide-react";
type ControlsProps = {
schedule: Schedule;
handleDelete: () => void;
handleDuplicate: () => void;
handleSetDefault: () => void;
};
export function Controls({ schedule, handleDelete, handleDuplicate, handleSetDefault }: ControlsProps) {
return (
<>
<DropdownMenu>
<DropdownMenuTrigger>
<Button type="button" color="secondary" className="bg-secondary text-secondary mx-5">
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!schedule.isDefault && (
<DropdownMenuItem
onClick={() => {
handleSetDefault();
}}
className="min-w-40 focus:ring-mute min-w-40 focus:ring-muted">
<Star />
Set as default
</DropdownMenuItem>
)}
<DropdownMenuItem
className="outline-none"
onClick={() => {
handleDuplicate();
}}>
<Copy />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="min-w-40 focus:ring-muted"
onClick={() => {
handleDelete();
}}>
<Trash />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Toaster />
</>
);
}

View File

@ -0,0 +1,66 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { LucideIcon as IconType } from "lucide-react";
import type { ReactNode } from "react";
import React from "react";
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={cn(
"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={cn("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";
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 | null;
eventTypeId: number | null;
date: Date | null;
days: number[];
scheduleId: number | null;
}[];
timeZone: string | null;
};
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>
);
}

View File

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.cjs",
"css": "globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -8,3 +8,76 @@
@tailwind utilities;
@import "../ui/styles/shared-globals.css";
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1 +1,3 @@
export { Booker } from "./booker/Booker";
export { AvailabilityList } from "./availability-list";
export { Availability } from "./availability-list/components/availability";

View File

@ -11,12 +11,27 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
"@types/node": "^20.8.3",
"@types/react": "18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^2.2.0",
"react-hook-form": "^7.47.0",
"rollup-plugin-node-builtins": "^2.1.2",
"typescript": "^4.9.4",
"vite": "^4.1.2"
},
"main": "./index"
"main": "./index",
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.284.0",
"react": "^18.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"
}
}

View File

@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,46 @@
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@ -0,0 +1,97 @@
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg md:w-full",
className
)}
{...props}>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,179 @@
import { cn } from "@/lib/utils";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("bg-muted -mx-1 my-1 h-px", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@ -0,0 +1,21 @@
import { cn } from "@/lib/utils";
import * as React from "react";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };

View File

@ -0,0 +1,18 @@
import { cn } from "@/lib/utils";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@ -0,0 +1,109 @@
import { cn } from "@/lib/utils";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"ring-offset-background hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@ -0,0 +1,31 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@ -0,0 +1,186 @@
// Inspired by react-hot-toast library
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
import * as React from "react";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -3,5 +3,78 @@ const base = require("@calcom/config/tailwind-preset");
/** @type {import('tailwindcss').Config} */
module.exports = {
...base,
content: ["./bookings/**/*.tsx"],
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"./bookings/**/*.tsx",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
};

View File

@ -3,7 +3,8 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["/*"]
"~/*": ["/*"],
"@/*": ["./src/*"]
},
"resolveJsonModule": true
},

View File

@ -1,8 +1,12 @@
import { resolve } from "path";
import react from "@vitejs/plugin-react";
import { resolve, path } from "path";
import { defineConfig } from "vite";
// setting up shadcn for vite: https://ui.shadcn.com/docs/installation/vite
export default defineConfig({
build: {
plugins: [react()],
lib: {
entry: [resolve(__dirname, "booker/export.ts")],
name: "CalAtoms",
@ -23,6 +27,7 @@ export default defineConfig({
fs: resolve("../../node_modules/rollup-plugin-node-builtins"),
path: resolve("../../node_modules/rollup-plugin-node-builtins"),
os: resolve("../../node_modules/rollup-plugin-node-builtins"),
"@": path.resolve(__dirname, "./src"),
},
},
});