diff --git a/package.json b/package.json index 063d6a5..a36b31a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@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-menubar": "^1.0.4", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43271b1..cfbd71e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-menubar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) @@ -2671,6 +2674,36 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.28)(react@18.2.0) dev: false + /@radix-ui/react-menubar@1.0.4(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bHgUo9gayKZfaQcWSSLr++LyS0rgh+MvD89DE4fJ6TkGHvjHgPaBZf44hdka7ogOxIOdj9163J+5xL2Dn4qzzg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.28)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.28)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.28)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.28)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.28)(react@18.2.0) + '@types/react': 18.2.28 + '@types/react-dom': 18.2.13 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 047892d..c12f2c3 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -6,8 +6,8 @@ interface DashboardLayoutProps { export default function DashboardLayout({ children }: DashboardLayoutProps) { return ( - <div className="container "> - <div className="flex min-h-[calc(100vh-140px)] flex-col gap-8 rounded-md border bg-primary-foreground px-5 py-8 md:min-h-[calc(100vh-160px)] lg:flex-row"> + <div className="container"> + <div className="flex min-h-[calc(100vh-140px)] flex-col gap-8 rounded-md py-8 md:min-h-[calc(100vh-160px)] lg:flex-row 2xl:gap-12"> <aside className="lg:w-1/5"> <SidebarNav /> </aside> diff --git a/src/app/dashboard/projects/action.ts b/src/app/dashboard/projects/action.ts index 65c3fa1..e7d76d7 100644 --- a/src/app/dashboard/projects/action.ts +++ b/src/app/dashboard/projects/action.ts @@ -53,6 +53,9 @@ export async function getProjects() { where: { userId: user.id, }, + orderBy: { + createdAt: "desc", + }, }); return projects as Project[]; } diff --git a/src/app/dashboard/projects/page.tsx b/src/app/dashboard/projects/page.tsx index c95eb44..0087b3c 100644 --- a/src/app/dashboard/projects/page.tsx +++ b/src/app/dashboard/projects/page.tsx @@ -7,7 +7,8 @@ export default async function Projects() { const projects = await getProjects(); return ( - <div className="grid grid-cols-4 gap-4 "> + <div className="grid gap-4 md:grid-cols-3 lg:grid-cols-4 "> + <CreateProjectModal /> {projects.map((project) => ( <Card role="button" @@ -24,8 +25,6 @@ export default async function Projects() { </Link> </Card> ))} - - <CreateProjectModal /> </div> ); } diff --git a/src/app/dashboard/settings/settings-form.tsx b/src/app/dashboard/settings/settings-form.tsx index 1cb31d2..44fb54b 100644 --- a/src/app/dashboard/settings/settings-form.tsx +++ b/src/app/dashboard/settings/settings-form.tsx @@ -115,14 +115,14 @@ export default function SettingsForm({ <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} - className="max-w-2xl space-y-8 " + className="max-w-2xl space-y-8 rounded-md border p-6 " > <FormField control={form.control} name="image" render={({ field }) => ( <FormItem> - <FormLabel>Image</FormLabel> + <FormLabel>Picture</FormLabel> <FormControl> <Avatar className="group relative h-28 w-28 rounded-full"> <AvatarImage src={field.value} alt={form.getValues().name} /> diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 0260321..1bc2eb2 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,12 +1,13 @@ import Image from "next/image"; import Link from "next/link"; import { siteConfig } from "~/config/site"; +import ThemeToggle from "../shared/theme-toggle"; export default function Footer() { return ( <footer className="relative z-10 w-full py-4 md:py-0"> - <div className="container flex flex-col items-center justify-between gap-4 md:h-20 md:flex-row"> - <div className="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0"> + <div className="container flex items-center justify-between gap-4 md:h-20 md:flex-row"> + <div className="flex flex-col items-center gap-4 md:flex-row md:gap-2"> <Image src="/chad-next.png" alt="ChadNext logo" @@ -26,20 +27,8 @@ export default function Footer() { </Link> </p> </div> - <p className="space-x-4 text-center text-sm leading-loose text-muted-foreground md:text-left"> - <Link - href="/about" - className="font-semibold hover:underline hover:underline-offset-4" - > - About - </Link> - <Link - href="/changelog" - className="font-semibold hover:underline hover:underline-offset-4" - > - Changelog - </Link> - </p> + + <ThemeToggle /> </div> </footer> ); diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index c8b60f0..00014db 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -4,5 +4,11 @@ import Navbar from "./navbar"; export default async function Header() { const currentUser = (await getUser()) as CurrentUser; - return <Navbar currentUser={currentUser} />; + return ( + <header className="h-20 w-full bg-transparent"> + <div className="container h-full"> + <Navbar loggedInUser={currentUser} /> + </div> + </header> + ); } diff --git a/src/components/layout/header/navbar.tsx b/src/components/layout/header/navbar.tsx index 40d6e3d..41d19c7 100644 --- a/src/components/layout/header/navbar.tsx +++ b/src/components/layout/header/navbar.tsx @@ -1,50 +1,94 @@ "use client"; +import { MenuIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; +import { useState } from "react"; import { buttonVariants } from "~/components/ui/button"; -import useScroll from "~/hooks/use-scroll"; -import { cn } from "~/lib/utils"; +import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"; import { type CurrentUser } from "~/types"; -import ThemeToggle from "../../shared/theme-toggle"; import UserNav from "../user-nav"; - -export default function Navbar({ currentUser }: { currentUser: CurrentUser }) { - const scrolled = useScroll(50); - +export default function Navbar({ + loggedInUser, +}: { + loggedInUser: CurrentUser; +}) { + const [isModalOpen, setIsModalOpen] = useState(false); return ( - <header - className={cn( - "sticky top-0 z-30 w-full bg-transparent ", - scrolled - ? " h-16 border-b backdrop-blur-xl transition-all duration-300 ease-in-out" - : " h-20" - )} - > - <div className="container h-full"> - <div className="flex h-full items-center justify-between"> - <Link href="/" className="flex items-center text-2xl font-bold"> - <Image - src="/chad-next.png" - alt="ChadNext logo" - width="30" - height="30" - className="mr-2 rounded-sm object-contain" - /> - <p>ChadNext</p> + <nav className="flex h-full items-center justify-between"> + <Link href="/" className="flex items-center text-2xl font-bold"> + <Image + src="/chad-next.png" + alt="ChadNext logo" + width="30" + height="30" + className="mr-2 rounded-sm object-contain" + /> + <p>ChadNext</p> + </Link> + <div className="hidden items-center gap-12 md:flex 2xl:gap-16"> + <div className="space-x-4 text-center text-sm leading-loose text-muted-foreground md:text-left"> + <Link + href="/changelog" + className="font-semibold hover:underline hover:underline-offset-4" + > + Changelog + </Link> + <Link + href="/about" + className="font-semibold hover:underline hover:underline-offset-4" + > + About </Link> - <div className=" flex items-center gap-x-2"> - <ThemeToggle /> - {currentUser ? ( - <UserNav user={currentUser} /> - ) : ( - <Link href="/signin" className={buttonVariants()}> - Sign In - </Link> - )} - </div> + </div> + <div className="flex items-center gap-x-2"> + {loggedInUser ? ( + <UserNav user={loggedInUser} /> + ) : ( + <Link href="/signin" className={buttonVariants()}> + Sign In + </Link> + )} </div> </div> - </header> + <Sheet open={isModalOpen} onOpenChange={setIsModalOpen}> + <SheetTrigger className="md:hidden"> + <MenuIcon /> + </SheetTrigger> + <SheetContent> + <div className="flex flex-col items-center space-y-10 py-10"> + <div className="space-y-4 text-center text-sm leading-loose text-muted-foreground"> + <Link + href="/changelog" + className="block font-semibold hover:underline hover:underline-offset-4" + onClick={() => setIsModalOpen(false)} + > + Changelog + </Link> + <Link + href="/about" + className="block font-semibold hover:underline hover:underline-offset-4" + onClick={() => setIsModalOpen(false)} + > + About + </Link> + </div> + <div className="flex items-center gap-x-2"> + {loggedInUser ? ( + <UserNav user={loggedInUser} /> + ) : ( + <Link + href="/signin" + className={buttonVariants()} + onClick={() => setIsModalOpen(false)} + > + Sign In + </Link> + )} + </div> + </div> + </SheetContent> + </Sheet> + </nav> ); } diff --git a/src/components/layout/user-nav.tsx b/src/components/layout/user-nav.tsx index f468042..84127cd 100644 --- a/src/components/layout/user-nav.tsx +++ b/src/components/layout/user-nav.tsx @@ -1,8 +1,9 @@ +"use client"; + import { LayoutDashboardIcon, LogOut } from "lucide-react"; import { signOut } from "next-auth/react"; import Link from "next/link"; import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; -import { Button } from "~/components/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -17,14 +18,12 @@ export default function UserNav({ user }: { user: CurrentUser }) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> - <Button variant="ghost" className="relative h-8 w-8 rounded-full"> - <Avatar className="h-8 w-8"> - <AvatarImage src={user.image || ""} alt={user.name || ""} /> - <AvatarFallback>{user.name?.[0]}</AvatarFallback> - </Avatar> - </Button> + <Avatar className="h-8 w-8"> + <AvatarImage src={user.image || ""} alt={user.name || ""} /> + <AvatarFallback>{user.name?.[0]}</AvatarFallback> + </Avatar> </DropdownMenuTrigger> - <DropdownMenuContent className="w-56" align="end" forceMount> + <DropdownMenuContent> <DropdownMenuLabel className="font-normal"> <div className="flex flex-col space-y-1"> <p className="text-sm font-medium leading-none">{user.name}</p> diff --git a/src/components/sections/hero.tsx b/src/components/sections/hero.tsx index 19e263b..996f8a1 100644 --- a/src/components/sections/hero.tsx +++ b/src/components/sections/hero.tsx @@ -31,7 +31,7 @@ export default async function Hero() { Introducing ChadNext </p> </a> - <h1 className="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-400 bg-clip-text text-center font-heading text-4xl font-bold tracking-[-0.02em] text-transparent drop-shadow-sm duration-300 ease-linear animate-in zoom-in-50 dark:bg-gradient-to-br dark:from-gray-100 dark:to-gray-900 md:text-7xl md:leading-[5rem]"> + <h1 className=" bg-gradient-to-br from-gray-900 via-gray-800 to-gray-400 bg-clip-text text-center font-heading text-[40px] font-bold leading-tight tracking-[-0.02em] text-transparent drop-shadow-sm duration-300 ease-linear animate-in zoom-in-50 dark:bg-gradient-to-br dark:from-gray-100 dark:to-gray-900 md:text-7xl md:leading-[5rem]"> <Balancer>Quick Starter Template for your Next.js project</Balancer> </h1> <p className="mt-6 text-center text-muted-foreground md:text-xl"> diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx new file mode 100644 index 0000000..d18ca1e --- /dev/null +++ b/src/components/ui/menubar.tsx @@ -0,0 +1,236 @@ +"use client"; + +import * as React from "react"; +import * as MenubarPrimitive from "@radix-ui/react-menubar"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "~/lib/utils"; + +const MenubarMenu = MenubarPrimitive.Menu; + +const MenubarGroup = MenubarPrimitive.Group; + +const MenubarPortal = MenubarPrimitive.Portal; + +const MenubarSub = MenubarPrimitive.Sub; + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup; + +const Menubar = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Root + ref={ref} + className={cn( + "flex h-10 items-center space-x-1 rounded-md border bg-background p-1", + className + )} + {...props} + /> +)); +Menubar.displayName = MenubarPrimitive.Root.displayName; + +const MenubarTrigger = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Trigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + className + )} + {...props} + /> +)); +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + <MenubarPrimitive.SubTrigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + inset && "pl-8", + className + )} + {...props} + > + {children} + <ChevronRight className="ml-auto h-4 w-4" /> + </MenubarPrimitive.SubTrigger> +)); +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; + +const MenubarSubContent = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.SubContent + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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", + className + )} + {...props} + /> +)); +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; + +const MenubarContent = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> +>( + ( + { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, + ref + ) => ( + <MenubarPrimitive.Portal> + <MenubarPrimitive.Content + ref={ref} + align={align} + alignOffset={alignOffset} + sideOffset={sideOffset} + className={cn( + "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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", + className + )} + {...props} + /> + </MenubarPrimitive.Portal> + ) +); +MenubarContent.displayName = MenubarPrimitive.Content.displayName; + +const MenubarItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + <MenubarPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + inset && "pl-8", + className + )} + {...props} + /> +)); +MenubarItem.displayName = MenubarPrimitive.Item.displayName; + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> +>(({ className, children, checked, ...props }, ref) => ( + <MenubarPrimitive.CheckboxItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground 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"> + <MenubarPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.CheckboxItem> +)); +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; + +const MenubarRadioItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> +>(({ className, children, ...props }, ref) => ( + <MenubarPrimitive.RadioItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground 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"> + <MenubarPrimitive.ItemIndicator> + <Circle className="h-2 w-2 fill-current" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.RadioItem> +)); +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; + +const MenubarLabel = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + <MenubarPrimitive.Label + ref={ref} + className={cn( + "px-2 py-1.5 text-sm font-semibold", + inset && "pl-8", + className + )} + {...props} + /> +)); +MenubarLabel.displayName = MenubarPrimitive.Label.displayName; + +const MenubarSeparator = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-muted", className)} + {...props} + /> +)); +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; + +const MenubarShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ); +}; +MenubarShortcut.displayname = "MenubarShortcut"; + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, +}; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..c7f768c --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client"; + +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "~/lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <SheetPrimitive.Overlay + className={cn( + "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + className + )} + {...props} + ref={ref} + /> +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, + VariantProps<typeof sheetVariants> {} + +const SheetContent = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Content>, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Content + ref={ref} + className={cn(sheetVariants({ side }), className)} + {...props} + > + {children} + <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> + <X className="h-4 w-4" /> + <span className="sr-only">Close</span> + </SheetPrimitive.Close> + </SheetPrimitive.Content> + </SheetPortal> +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col space-y-2 text-center sm:text-left", + className + )} + {...props} + /> +); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", + className + )} + {...props} + /> +); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> +>(({ className, ...props }, ref) => ( + <SheetPrimitive.Title + ref={ref} + className={cn("text-lg font-semibold text-foreground", className)} + {...props} + /> +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> +>(({ className, ...props }, ref) => ( + <SheetPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +};