Spaces:
Running
Running
"use client"; | |
import { cn } from "@/lib/utils"; | |
import { Link } from "react-router-dom"; | |
import React, { useState, createContext, useContext } from "react"; | |
import { AnimatePresence, motion } from "framer-motion"; | |
import { Menu, X } from "lucide-react"; | |
interface Links { | |
label: string; | |
href: string; | |
icon: React.JSX.Element | React.ReactNode; | |
} | |
interface SidebarContextProps { | |
open: boolean; | |
setOpen: React.Dispatch<React.SetStateAction<boolean>>; | |
animate: boolean; | |
} | |
const SidebarContext = createContext<SidebarContextProps | undefined>( | |
undefined | |
); | |
export const useSidebar = () => { | |
const context = useContext(SidebarContext); | |
if (!context) { | |
throw new Error("useSidebar must be used within a SidebarProvider"); | |
} | |
return context; | |
}; | |
export const SidebarProvider = ({ | |
children, | |
open: openProp, | |
setOpen: setOpenProp, | |
animate = true, | |
}: { | |
children: React.ReactNode; | |
open?: boolean; | |
setOpen?: React.Dispatch<React.SetStateAction<boolean>>; | |
animate?: boolean; | |
}) => { | |
const [openState, setOpenState] = useState(false); | |
const open = openProp !== undefined ? openProp : openState; | |
const setOpen = setOpenProp !== undefined ? setOpenProp : setOpenState; | |
return ( | |
<SidebarContext.Provider value={{ open, setOpen, animate }}> | |
{children} | |
</SidebarContext.Provider> | |
); | |
}; | |
export const Sidebar = ({ | |
children, | |
open, | |
setOpen, | |
animate, | |
}: { | |
children: React.ReactNode; | |
open?: boolean; | |
setOpen?: React.Dispatch<React.SetStateAction<boolean>>; | |
animate?: boolean; | |
}) => { | |
return ( | |
<SidebarProvider open={open} setOpen={setOpen} animate={animate}> | |
{children} | |
</SidebarProvider> | |
); | |
}; | |
export const SidebarBody = (props: React.ComponentProps<typeof motion.div>) => { | |
return ( | |
<> | |
<DesktopSidebar {...props} /> | |
<MobileSidebar {...(props as unknown as React.ComponentProps<"div">)} /> | |
</> | |
); | |
}; | |
export const DesktopSidebar = ({ | |
className, | |
children, | |
...props | |
}: React.ComponentProps<typeof motion.div>) => { | |
const { open, animate } = useSidebar(); | |
return ( | |
<motion.div | |
className={cn( | |
"h-full px-4 py-4 hidden md:flex md:flex-col bg-sidebar text-sidebar-foreground w-[300px] flex-shrink-0", | |
className | |
)} | |
animate={{ | |
width: animate ? (open ? "300px" : "60px") : "300px", | |
}} | |
{...props} | |
> | |
{children} | |
</motion.div> | |
); | |
}; | |
export const MobileSidebar = ({ | |
className, | |
children, | |
...props | |
}: React.ComponentProps<"div">) => { | |
const { open, setOpen } = useSidebar(); | |
return ( | |
<> | |
<div | |
className={cn( | |
"h-10 px-4 py-4 flex flex-row md:hidden items-center justify-between bg-sidebar text-sidebar-foreground w-full" | |
)} | |
{...props} | |
> | |
<div className="flex justify-end z-20 w-full"> | |
<Menu className="cursor-pointer" onClick={() => setOpen(!open)} /> | |
</div> | |
<AnimatePresence> | |
{open && ( | |
<motion.div | |
initial={{ x: "-100%", opacity: 0 }} | |
animate={{ x: 0, opacity: 1 }} | |
exit={{ x: "-100%", opacity: 0 }} | |
transition={{ | |
duration: 0.3, | |
ease: "easeInOut", | |
}} | |
className={cn( | |
"fixed h-full w-full inset-0 bg-background text-foreground p-10 z-[100] flex flex-col justify-between", | |
className | |
)} | |
> | |
<div | |
className="absolute right-10 top-10 z-50 cursor-pointer" | |
onClick={() => setOpen(!open)} | |
> | |
<X /> | |
</div> | |
{children} | |
</motion.div> | |
)} | |
</AnimatePresence> | |
</div> | |
</> | |
); | |
}; | |
export const SidebarLink = ({ | |
link, | |
className, | |
...props | |
}: { | |
link: Links; | |
className?: string; | |
}) => { | |
const { open, animate } = useSidebar(); | |
return ( | |
<Link | |
to={link.href} | |
className={cn( | |
"flex items-center justify-start gap-2 group/sidebar py-2", | |
className | |
)} | |
{...props} | |
> | |
{link.icon} | |
<motion.span | |
animate={{ | |
display: animate ? (open ? "inline-block" : "none") : "inline-block", | |
opacity: animate ? (open ? 1 : 0) : 1, | |
}} | |
className="text-sm group-hover/sidebar:translate-x-1 transition duration-150 whitespace-pre inline-block !p-0 !m-0 font-mono" | |
> | |
{link.label} | |
</motion.span> | |
</Link> | |
); | |
}; | |