diff --git a/package.json b/package.json
index c5c77fb..c1273c1 100644
--- a/package.json
+++ b/package.json
@@ -27,20 +27,23 @@
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "^5.0.1",
"@prisma/client": "^6.9.0",
+ "@radix-ui/react-collapsible": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-select": "^2.2.4",
- "@radix-ui/react-separator": "^1.1.6",
- "@radix-ui/react-slot": "^1.2.2",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.80.7",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "lucide-react": "^0.511.0",
+ "lucide-react": "^0.515.0",
"next": "15.4.0-canary.92",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6",
diff --git a/src/app/home/page.tsx b/src/app/(main)/home/page.tsx
similarity index 81%
rename from src/app/home/page.tsx
rename to src/app/(main)/home/page.tsx
index 77f3cf8..c381c03 100644
--- a/src/app/home/page.tsx
+++ b/src/app/(main)/home/page.tsx
@@ -1,15 +1,13 @@
'use client';
import { RedirectButton } from '@/components/buttons/redirect-button';
-import { ThemePicker } from '@/components/misc/theme-picker';
import { useGetApiUserMe } from '@/generated/api/user/user';
export default function Home() {
const { data, isLoading } = useGetApiUserMe();
return (
-
-
{}
+
Hello{' '}
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
new file mode 100644
index 0000000..7106e70
--- /dev/null
+++ b/src/app/(main)/layout.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { cookies } from 'next/headers';
+
+import { AppSidebar } from '@/components/custom-ui/app-sidebar';
+import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider';
+import Header from '@/components/misc/header';
+
+export default async function Layout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const cookieStore = await cookies();
+ const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true';
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index f85cb2f..a5f7eaf 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -55,6 +55,8 @@
--card: var(--neutral-800);
+ --sidebar-width-icon: 32px;
+
/* ------------------- */
--foreground: oklch(0.13 0.028 261.692);
@@ -95,17 +97,17 @@
--chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0.002 247.839);
+ --sidebar: var(--background);
- --sidebar-foreground: oklch(0.13 0.028 261.692);
+ --sidebar-foreground: var(--text);
--sidebar-primary: oklch(0.21 0.034 264.665);
- --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
+ --sidebar-primary-foreground: var(--text);
--sidebar-accent: oklch(0.967 0.003 264.542);
- --sidebar-accent-foreground: oklch(0.21 0.034 264.665);
+ --sidebar-accent-foreground: var(--text);
--sidebar-border: oklch(0.928 0.006 264.531);
@@ -339,17 +341,17 @@
--chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.21 0.034 264.665);
+ --sidebar: var(--background);
- --sidebar-foreground: oklch(0.985 0.002 247.839);
+ --sidebar-foreground: var(--text);
--sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
+ --sidebar-primary-foreground: var(--text);
--sidebar-accent: oklch(0.278 0.033 256.848);
- --sidebar-accent-foreground: oklch(0.985 0.002 247.839);
+ --sidebar-accent-foreground: var(--text);
--sidebar-border: oklch(1 0 0 / 10%);
diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx
new file mode 100644
index 0000000..b279c73
--- /dev/null
+++ b/src/components/custom-ui/app-sidebar.tsx
@@ -0,0 +1,147 @@
+'use client';
+
+import React from 'react';
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+} from '@/components/ui/sidebar';
+
+import { ChevronDown } from 'lucide-react';
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from '@/components/ui/collapsible';
+
+import Logo from '@/components/misc/logo';
+
+import Link from 'next/link';
+
+import { ThemePicker } from '@/components/misc/theme-picker';
+
+import {
+ Star,
+ CalendarDays,
+ User,
+ Users,
+ CalendarClock,
+ CalendarPlus,
+} from 'lucide-react';
+
+const items = [
+ {
+ title: 'Calendar',
+ url: '#',
+ icon: CalendarDays,
+ },
+ {
+ title: 'Friends',
+ url: '#',
+ icon: User,
+ },
+ {
+ title: 'Groups',
+ url: '#',
+ icon: Users,
+ },
+ {
+ title: 'Events',
+ url: '#',
+ icon: CalendarClock,
+ },
+];
+
+export function AppSidebar() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {' '}
+
+ Favorites
+
+
+
+
+
+
+
+
+
+
+
+
+ {items.map((item) => (
+
+
+
+
+
+ {item.title}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ New Event
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx
new file mode 100644
index 0000000..dbf8a1f
--- /dev/null
+++ b/src/components/misc/header.tsx
@@ -0,0 +1,23 @@
+import { SidebarTrigger } from '@/components/ui/sidebar';
+import { ThemePicker } from '@/components/misc/theme-picker';
+
+export default function Header({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/misc/logo.tsx b/src/components/misc/logo.tsx
index 129adef..739fc90 100644
--- a/src/components/misc/logo.tsx
+++ b/src/components/misc/logo.tsx
@@ -90,6 +90,7 @@ export default function Logo({
return (
) {
+ return ;
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 3b4f1ef..3234cdc 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -13,10 +13,13 @@ function Separator({
}: React.ComponentProps) {
return (
);
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..84649ad
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left"
+}) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..6b68b8f
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,725 @@
+'use client';
+
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, VariantProps } from 'class-variance-authority';
+import { PanelLeftIcon } from 'lucide-react';
+
+import { useIsMobile } from '@/hooks/use-mobile';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { Skeleton } from '@/components/ui/skeleton';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+
+const SIDEBAR_COOKIE_NAME = 'sidebar_state';
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = '16rem';
+const SIDEBAR_WIDTH_MOBILE = '18rem';
+const SIDEBAR_WIDTH_ICON = '4rem';
+const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
+
+type SidebarContextProps = {
+ state: 'expanded' | 'collapsed';
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error('useSidebar must be used within a SidebarProvider.');
+ }
+
+ return context;
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}) {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === 'function' ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? 'expanded' : 'collapsed';
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function Sidebar({
+ side = 'left',
+ variant = 'sidebar',
+ collapsible = 'offcanvas',
+ className,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & {
+ side?: 'left' | 'right';
+ variant?: 'sidebar' | 'floating' | 'inset';
+ collapsible?: 'offcanvas' | 'icon' | 'none';
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === 'none') {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
+ return (
+
+ );
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'div';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0 ml-[7.5px]',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+const sidebarMenuButtonVariants = cva(
+ 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! p-0 ml-[15.5px] [&>span:last-child]:truncate [&>svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
+ outline:
+ 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
+ },
+ size: {
+ default: 'h-8 text-sm',
+ sm: 'h-7 text-xs',
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = 'default',
+ size = 'default',
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+} & VariantProps) {
+ const Comp = asChild ? Slot : 'button';
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === 'string') {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+}) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'peer-data-[size=sm]/menu-button:top-1',
+ 'peer-data-[size=default]/menu-button:top-1.5',
+ 'peer-data-[size=lg]/menu-button:top-2.5',
+ 'group-data-[collapsible=icon]:hidden',
+ showOnHover &&
+ 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<'div'> & {
+ showIcon?: boolean;
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = 'md',
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean;
+ size?: 'sm' | 'md';
+ isActive?: boolean;
+}) {
+ const Comp = asChild ? Slot : 'a';
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
+ 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
+ size === 'sm' && 'text-xs',
+ size === 'md' && 'text-sm',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..32ea0ef
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..4ee26b3
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx
new file mode 100644
index 0000000..3873fa4
--- /dev/null
+++ b/src/components/wrappers/sidebar-provider.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import React from 'react';
+import { SidebarProvider } from '../ui/sidebar';
+
+export default function SidebarProviderWrapper({
+ defaultOpen,
+ children,
+}: {
+ defaultOpen: boolean;
+ children: React.ReactNode;
+}) {
+ const [open, setOpen] = React.useState(defaultOpen);
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/yarn.lock b/yarn.lock
index 56de1d1..e1ff28c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -543,15 +543,15 @@ __metadata:
linkType: hard
"@gerrit0/mini-shiki@npm:^3.2.2":
- version: 3.6.0
- resolution: "@gerrit0/mini-shiki@npm:3.6.0"
+ version: 3.7.0
+ resolution: "@gerrit0/mini-shiki@npm:3.7.0"
dependencies:
- "@shikijs/engine-oniguruma": "npm:^3.6.0"
- "@shikijs/langs": "npm:^3.6.0"
- "@shikijs/themes": "npm:^3.6.0"
- "@shikijs/types": "npm:^3.6.0"
+ "@shikijs/engine-oniguruma": "npm:^3.7.0"
+ "@shikijs/langs": "npm:^3.7.0"
+ "@shikijs/themes": "npm:^3.7.0"
+ "@shikijs/types": "npm:^3.7.0"
"@shikijs/vscode-textmate": "npm:^10.0.2"
- checksum: 10c0/347456c9da8a1fadd3c1f63097da459a5f930ef4bca6431cce913a379012c551e061d0a94ff7a0f307215b87f2418b7c198a55fba888fc97fb02ab36247adf6b
+ checksum: 10c0/eb3f4900d841338077d839ebbc7f8722b13876a586cff7abc73295e956683724dd3371a9f990900184a2d069461965951b2604d677991badf3474262e7811384
languageName: node
linkType: hard
@@ -1299,6 +1299,32 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-collapsible@npm:^1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-collapsible@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/fa2de539ef06e2b2d18acebb12a34ce1534ca88bd484b7359aac05534d1e551fe83eaafbf60915c00161bb370f0dc9fc303903133510dea0a59fd018155b7db5
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-collection@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-collection@npm:1.1.7"
@@ -1347,6 +1373,38 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-dialog@npm:^1.1.14":
+ version: 1.1.14
+ resolution: "@radix-ui/react-dialog@npm:1.1.14"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.10"
+ "@radix-ui/react-focus-guards": "npm:1.1.2"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/ab7bc783510ed8fccfe91020b214f4a571d5a1d46d398faa33f4c151bc9f586c47483b307e72b67687b06694c194b3aa80dd1de728460fa765db9f3057690ba3
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-direction@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-direction@npm:1.1.1"
@@ -1719,7 +1777,7 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-separator@npm:^1.1.6":
+"@radix-ui/react-separator@npm:^1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-separator@npm:1.1.7"
dependencies:
@@ -1738,7 +1796,7 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.2":
+"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.3":
version: 1.2.3
resolution: "@radix-ui/react-slot@npm:1.2.3"
dependencies:
@@ -1804,6 +1862,36 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-tooltip@npm:^1.2.7":
+ version: 1.2.7
+ resolution: "@radix-ui/react-tooltip@npm:1.2.7"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.10"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.7"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/28798d576c6ffec4f11120cd563aa9d5ab9afb9a37dc18778176442756d026c8c46eec1ddc647b2b5914045495fcb89f82530106e91acb55776b7d6b1a10fb57
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-callback-ref@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1"
@@ -1966,7 +2054,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/engine-oniguruma@npm:^3.6.0":
+"@shikijs/engine-oniguruma@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/engine-oniguruma@npm:3.7.0"
dependencies:
@@ -1976,7 +2064,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/langs@npm:^3.6.0":
+"@shikijs/langs@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/langs@npm:3.7.0"
dependencies:
@@ -1985,7 +2073,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/themes@npm:^3.6.0":
+"@shikijs/themes@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/themes@npm:3.7.0"
dependencies:
@@ -1994,7 +2082,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.6.0":
+"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/types@npm:3.7.0"
dependencies:
@@ -3662,7 +3750,7 @@ __metadata:
languageName: node
linkType: hard
-"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8":
+"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9":
version: 3.1.9
resolution: "array-includes@npm:3.1.9"
dependencies:
@@ -3699,7 +3787,7 @@ __metadata:
languageName: node
linkType: hard
-"array.prototype.findlastindex@npm:^1.2.5":
+"array.prototype.findlastindex@npm:^1.2.6":
version: 1.2.6
resolution: "array.prototype.findlastindex@npm:1.2.6"
dependencies:
@@ -3714,7 +3802,7 @@ __metadata:
languageName: node
linkType: hard
-"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2":
+"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3":
version: 1.3.3
resolution: "array.prototype.flat@npm:1.3.3"
dependencies:
@@ -4792,7 +4880,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint-module-utils@npm:^2.12.0":
+"eslint-module-utils@npm:^2.12.1":
version: 2.12.1
resolution: "eslint-module-utils@npm:2.12.1"
dependencies:
@@ -4805,31 +4893,31 @@ __metadata:
linkType: hard
"eslint-plugin-import@npm:^2.31.0":
- version: 2.31.0
- resolution: "eslint-plugin-import@npm:2.31.0"
+ version: 2.32.0
+ resolution: "eslint-plugin-import@npm:2.32.0"
dependencies:
"@rtsao/scc": "npm:^1.1.0"
- array-includes: "npm:^3.1.8"
- array.prototype.findlastindex: "npm:^1.2.5"
- array.prototype.flat: "npm:^1.3.2"
- array.prototype.flatmap: "npm:^1.3.2"
+ array-includes: "npm:^3.1.9"
+ array.prototype.findlastindex: "npm:^1.2.6"
+ array.prototype.flat: "npm:^1.3.3"
+ array.prototype.flatmap: "npm:^1.3.3"
debug: "npm:^3.2.7"
doctrine: "npm:^2.1.0"
eslint-import-resolver-node: "npm:^0.3.9"
- eslint-module-utils: "npm:^2.12.0"
+ eslint-module-utils: "npm:^2.12.1"
hasown: "npm:^2.0.2"
- is-core-module: "npm:^2.15.1"
+ is-core-module: "npm:^2.16.1"
is-glob: "npm:^4.0.3"
minimatch: "npm:^3.1.2"
object.fromentries: "npm:^2.0.8"
object.groupby: "npm:^1.0.3"
- object.values: "npm:^1.2.0"
+ object.values: "npm:^1.2.1"
semver: "npm:^6.3.1"
- string.prototype.trimend: "npm:^1.0.8"
+ string.prototype.trimend: "npm:^1.0.9"
tsconfig-paths: "npm:^3.15.0"
peerDependencies:
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
- checksum: 10c0/e21d116ddd1900e091ad120b3eb68c5dd5437fe2c930f1211781cd38b246f090a6b74d5f3800b8255a0ed29782591521ad44eb21c5534960a8f1fb4040fd913a
+ checksum: 10c0/bfb1b8fc8800398e62ddfefbf3638d185286edfed26dfe00875cc2846d954491b4f5112457831588b757fa789384e1ae585f812614c4797f0499fa234fd4a48b
languageName: node
linkType: hard
@@ -5758,7 +5846,7 @@ __metadata:
languageName: node
linkType: hard
-"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0":
+"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1":
version: 2.16.1
resolution: "is-core-module@npm:2.16.1"
dependencies:
@@ -6454,12 +6542,12 @@ __metadata:
languageName: node
linkType: hard
-"lucide-react@npm:^0.511.0":
- version: 0.511.0
- resolution: "lucide-react@npm:0.511.0"
+"lucide-react@npm:^0.515.0":
+ version: 0.515.0
+ resolution: "lucide-react@npm:0.515.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
- checksum: 10c0/bf09dd73cf2233abea90506ad31a91739555d761062722acbe045cb73e274f035b196472de0971a8a8f0645b2b54e3f21b8c1980fe87c909ca93171a9c28428a
+ checksum: 10c0/00485e09ab3d0bbb34797b1f368c269e8708522b6e2f46fd84dd5bd99741546487be9a65a260f274e8049b81cc37687566e26132f5752352c8d9bc8e5d0b3dea
languageName: node
linkType: hard
@@ -6549,15 +6637,18 @@ __metadata:
"@fortawesome/react-fontawesome": "npm:^0.2.2"
"@hookform/resolvers": "npm:^5.0.1"
"@prisma/client": "npm:^6.9.0"
+ "@radix-ui/react-collapsible": "npm:^1.1.11"
+ "@radix-ui/react-dialog": "npm:^1.1.14"
"@radix-ui/react-dropdown-menu": "npm:^2.1.14"
"@radix-ui/react-hover-card": "npm:^1.1.13"
"@radix-ui/react-label": "npm:^2.1.6"
"@radix-ui/react-scroll-area": "npm:^1.2.8"
"@radix-ui/react-select": "npm:^2.2.4"
- "@radix-ui/react-separator": "npm:^1.1.6"
- "@radix-ui/react-slot": "npm:^1.2.2"
+ "@radix-ui/react-separator": "npm:^1.1.7"
+ "@radix-ui/react-slot": "npm:^1.2.3"
"@radix-ui/react-switch": "npm:^1.2.4"
"@radix-ui/react-tabs": "npm:^1.1.11"
+ "@radix-ui/react-tooltip": "npm:^1.2.7"
"@tailwindcss/postcss": "npm:4.1.10"
"@tanstack/react-query": "npm:^5.80.7"
"@types/node": "npm:22.15.32"
@@ -6572,7 +6663,7 @@ __metadata:
eslint: "npm:9.29.0"
eslint-config-next: "npm:15.3.4"
eslint-config-prettier: "npm:10.1.5"
- lucide-react: "npm:^0.511.0"
+ lucide-react: "npm:^0.515.0"
next: "npm:15.4.0-canary.92"
next-auth: "npm:^5.0.0-beta.25"
next-themes: "npm:^0.4.6"
@@ -7185,7 +7276,7 @@ __metadata:
languageName: node
linkType: hard
-"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1":
+"object.values@npm:^1.1.6, object.values@npm:^1.2.1":
version: 1.2.1
resolution: "object.values@npm:1.2.1"
dependencies:
@@ -8569,7 +8660,7 @@ __metadata:
languageName: node
linkType: hard
-"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9":
+"string.prototype.trimend@npm:^1.0.9":
version: 1.0.9
resolution: "string.prototype.trimend@npm:1.0.9"
dependencies: