diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx
index 6ce3f59a..a28aff89 100644
--- a/apps/web/src/app/admin/layout.tsx
+++ b/apps/web/src/app/admin/layout.tsx
@@ -1,15 +1,17 @@
-import c from "config";
-import Image from "next/image";
-import Link from "next/link";
-import { Button } from "@/components/shadcn/ui/button";
-import DashNavItem from "@/components/dash/shared/DashNavItem";
import FullScreenMessage from "@/components/shared/FullScreenMessage";
-import ProfileButton from "@/components/shared/ProfileButton";
import React, { Suspense } from "react";
import ClientToast from "@/components/shared/ClientToast";
-import { isUserAdmin, userHasPermission } from "../../lib/utils/server/admin";
-import { PermissionType } from "@/lib/constants/permission";
+import { isUserAdmin } from "../../lib/utils/server/admin";
import { getCurrentUser } from "@/lib/utils/server/user";
+import { AdminSidebar } from "@/components/admin/shared/sidebar/AdminSidebar";
+import { Separator } from "@/components/shadcn/ui/separator";
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/shadcn/ui/sidebar";
+import { AdminBreadcrumbs } from "@/components/admin/shared/AdminBreadcrumbs";
+import { NavUserProfile } from "@/components/admin/shared/NavUserProfile";
interface AdminLayoutProps {
children: React.ReactNode;
@@ -19,11 +21,10 @@ export default async function AdminLayout({ children }: AdminLayoutProps) {
const user = await getCurrentUser();
if (!isUserAdmin(user)) {
- console.log("Denying admin access to user", user);
return (
);
}
@@ -31,76 +32,29 @@ export default async function AdminLayout({ children }: AdminLayoutProps) {
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {Object.entries(c.dashPaths.admin).map(([name, path]) => {
- // Gate specific admin nav items by permission
- if (
- name === "Users" &&
- !userHasPermission(user, PermissionType.VIEW_USERS)
- )
- return null;
- if (
- name === "Events" &&
- !userHasPermission(user, PermissionType.VIEW_EVENTS)
- )
- return null;
- if (
- name === "Roles" &&
- !userHasPermission(user, PermissionType.VIEW_ROLES)
- )
- return null;
- if (
- name === "Toggles" &&
- !userHasPermission(user, PermissionType.MANAGE_NAVLINKS)
- )
- return null;
- // Keep other configured admin paths visible by default
- return ;
- })}
-
-
Loading...}>{children}
+
+
+
+
+
+ Loading...}>
+ {children}
+
+
+
+
>
);
}
diff --git a/apps/web/src/app/admin/page.tsx b/apps/web/src/app/admin/page.tsx
index e02bb336..79b06ed7 100644
--- a/apps/web/src/app/admin/page.tsx
+++ b/apps/web/src/app/admin/page.tsx
@@ -30,7 +30,7 @@ export default async function Page() {
const timezone = getClientTimeZone(c.hackathonTimezone);
return (
-
+
Welcome,
diff --git a/apps/web/src/app/admin/roles/page.tsx b/apps/web/src/app/admin/roles/page.tsx
index f5bcc550..2c76b73c 100644
--- a/apps/web/src/app/admin/roles/page.tsx
+++ b/apps/web/src/app/admin/roles/page.tsx
@@ -15,7 +15,7 @@ export default async function Page() {
// pass serializable data to client
return (
-
+
Roles
+
Navbar Items
diff --git a/apps/web/src/app/admin/toggles/layout.tsx b/apps/web/src/app/admin/toggles/layout.tsx
index db8db932..0a2eff34 100644
--- a/apps/web/src/app/admin/toggles/layout.tsx
+++ b/apps/web/src/app/admin/toggles/layout.tsx
@@ -16,7 +16,7 @@ export default async function Layout({ children }: ToggleLayoutProps) {
return notFound();
return (
-
+
diff --git a/apps/web/src/app/admin/users/[slug]/page.tsx b/apps/web/src/app/admin/users/[slug]/page.tsx
index d4cfcd23..a72ce8b9 100644
--- a/apps/web/src/app/admin/users/[slug]/page.tsx
+++ b/apps/web/src/app/admin/users/[slug]/page.tsx
@@ -48,7 +48,7 @@ export default async function Page({ params }: { params: { slug: string } }) {
});
return (
-
+
{!!banInstance && (
diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx
index aebc72e2..d1a6d16b 100644
--- a/apps/web/src/app/admin/users/page.tsx
+++ b/apps/web/src/app/admin/users/page.tsx
@@ -17,7 +17,7 @@ export default async function Page() {
const userData = await getAllUsers();
return (
-
+
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 8494cab5..69b2adac 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -42,6 +42,14 @@
--gradient-color-2: #3366ff;
--gradient-color-3: #002db3;
--gradient-color-4: #1952cc;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -75,6 +83,22 @@
--destructive-foreground: 0 85.7% 97.3%;
--ring: 240 3.7% 15.9%;
+
+ --sidebar-background: 240 5.9% 10%;
+
+ --sidebar-foreground: 240 4.8% 95.9%;
+
+ --sidebar-primary: 224.3 76.3% 48%;
+
+ --sidebar-primary-foreground: 0 0% 100%;
+
+ --sidebar-accent: 240 3.7% 15.9%;
+
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+
+ --sidebar-border: 240 3.7% 15.9%;
+
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}
diff --git a/apps/web/src/components/admin/scanner/CheckinScanner.tsx b/apps/web/src/components/admin/scanner/CheckinScanner.tsx
index 8b82565e..613fad0a 100644
--- a/apps/web/src/components/admin/scanner/CheckinScanner.tsx
+++ b/apps/web/src/components/admin/scanner/CheckinScanner.tsx
@@ -127,9 +127,9 @@ export default function CheckinScanner({
return (
<>
-
-
-
+
+
+
{
const params = new URLSearchParams(
@@ -156,7 +156,7 @@ export default function CheckinScanner({
onError={(error) => console.log(error)}
styles={{
container: {
- width: "100vw",
+ width: "100%",
maxWidth: "500px",
margin: "0",
},
diff --git a/apps/web/src/components/admin/shared/AdminBreadcrumbs.tsx b/apps/web/src/components/admin/shared/AdminBreadcrumbs.tsx
new file mode 100644
index 00000000..6024f89e
--- /dev/null
+++ b/apps/web/src/components/admin/shared/AdminBreadcrumbs.tsx
@@ -0,0 +1,70 @@
+// app/admin/_components/admin-breadcrumbs.tsx
+"use client";
+
+import Link from "next/link";
+import { useSelectedLayoutSegments } from "next/navigation";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/shadcn/ui/breadcrumb";
+import { BreadcrumbLabels } from "@/lib/constants/admin";
+import React from "react";
+
+function formatSegment(segment: string) {
+ return (
+ BreadcrumbLabels[segment] ??
+ decodeURIComponent(segment)
+ .replace(/-/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase())
+ );
+}
+
+export function AdminBreadcrumbs() {
+ const segments = useSelectedLayoutSegments();
+
+ const visibleSegments = segments.filter(
+ (segment) => !segment.startsWith("(") && !segment.startsWith("@"),
+ );
+
+ const allSegments = ["admin", ...visibleSegments];
+
+ return (
+
+
+ {allSegments.map((segment, index) => {
+ const href =
+ "/" + allSegments.slice(0, index + 1).join("/");
+ const isLast = index === allSegments.length - 1;
+
+ return (
+
+
+ {isLast ? (
+
+ {formatSegment(segment)}
+
+ ) : (
+
+
+ {formatSegment(segment)}
+
+
+ )}
+
+
+ {!isLast && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/components/admin/shared/NavUserProfile.tsx b/apps/web/src/components/admin/shared/NavUserProfile.tsx
new file mode 100644
index 00000000..e62ce4c6
--- /dev/null
+++ b/apps/web/src/components/admin/shared/NavUserProfile.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { ChevronsUpDown } from "lucide-react";
+
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/components/shadcn/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/shadcn/ui/dropdown-menu";
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/shadcn/ui/sidebar";
+import { UserWithRole } from "db/types";
+import { SignOutButton } from "@clerk/nextjs";
+import Link from "next/link";
+import { DropdownSwitcher } from "@/components/shared/ThemeSwitcher";
+import Restricted from "@/components/Restricted";
+import { PermissionType } from "@/lib/constants/permission";
+
+export function NavUserProfile({ user }: { user: UserWithRole }) {
+ return (
+
+
+
+
+
+
+
+
+ {user.firstName.charAt(0) +
+ user.lastName.charAt(0)}
+
+
+
+
+ {user.firstName + " " + user.lastName}
+
+
+ {user.email}
+
+
+
+
+
+
+
+
+ Profile
+
+
+
+
+ Event Pass
+
+
+
+
+
+
+ Admin
+
+
+
+
+
+
+ Report a Bug
+
+
+
+
+ Settings
+
+
+
+
+
+
+
+ Sign out
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/admin/shared/sidebar/AdminSidebar.tsx b/apps/web/src/components/admin/shared/sidebar/AdminSidebar.tsx
new file mode 100644
index 00000000..77650268
--- /dev/null
+++ b/apps/web/src/components/admin/shared/sidebar/AdminSidebar.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import * as React from "react";
+import Image from "next/image";
+import Link from "next/link";
+
+import { NavMain } from "./NavMain";
+import { NavSecondary } from "./NavSecondary";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/shadcn/ui/sidebar";
+import c from "config";
+import { UserWithRole } from "db/types";
+import { userHasPermission } from "@/lib/utils/server/admin";
+import { adminSidebarData as data } from "@/lib/constants/admin";
+
+export function AdminSidebar({
+ user,
+ ...props
+}: React.ComponentProps & {
+ user?: UserWithRole;
+}) {
+ const mainItems = data.navMain.filter((item) =>
+ item.permission && user
+ ? userHasPermission(user, item.permission)
+ : !item.permission || !user,
+ );
+ const secondaryItems = data.navSecondary.filter((item) =>
+ item.permission && user
+ ? userHasPermission(user, item.permission)
+ : !item.permission || !user,
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Powered by HackKit
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/admin/shared/sidebar/NavMain.tsx b/apps/web/src/components/admin/shared/sidebar/NavMain.tsx
new file mode 100644
index 00000000..b753e471
--- /dev/null
+++ b/apps/web/src/components/admin/shared/sidebar/NavMain.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { ChevronRight, type LucideIcon } from "lucide-react";
+import { usePathname } from "next/navigation";
+
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/shadcn/ui/collapsible";
+import {
+ SidebarGroup,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+} from "@/components/shadcn/ui/sidebar";
+
+export function NavMain({
+ items,
+}: {
+ items: {
+ title: string;
+ url: string;
+ icon: LucideIcon;
+ isActive?: boolean;
+ items?: {
+ title: string;
+ url: string;
+ }[];
+ }[];
+}) {
+ const pathname = usePathname();
+
+ const isUrlActive = (itemUrl: string) => {
+ if (!pathname) return false;
+ return (
+ pathname === itemUrl ||
+ pathname.startsWith(`${itemUrl}/`) ||
+ pathname.startsWith(`${itemUrl}?`)
+ );
+ };
+
+ const isUrlExactActive = (itemUrl: string) => {
+ if (!pathname) return false;
+ return pathname === itemUrl || pathname.startsWith(`${itemUrl}?`);
+ };
+
+ return (
+
+
+ {items.map((item) => {
+ const hasSubItems = (item.items?.length ?? 0) > 0;
+ const isItemActiveSelf = isUrlExactActive(item.url);
+ const isAnySubItemActive = item.items?.some((subItem) =>
+ isUrlExactActive(subItem.url),
+ );
+ const shouldBeOpen =
+ hasSubItems &&
+ (isUrlActive(item.url) || isAnySubItemActive);
+
+ return (
+
+
+
+
+
+ {item.title}
+
+
+ {hasSubItems ? (
+ <>
+
+
+
+
+
+
+
+ {item.items?.map((subItem) => {
+ const isSubItemActive =
+ isUrlActive(
+ subItem.url,
+ );
+
+ return (
+
+
+
+
+ {
+ subItem.title
+ }
+
+
+
+
+ );
+ })}
+
+
+ >
+ ) : null}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/components/admin/shared/sidebar/NavSecondary.tsx b/apps/web/src/components/admin/shared/sidebar/NavSecondary.tsx
new file mode 100644
index 00000000..54eb13b8
--- /dev/null
+++ b/apps/web/src/components/admin/shared/sidebar/NavSecondary.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import * as React from "react";
+import { type LucideIcon } from "lucide-react";
+import { usePathname } from "next/navigation";
+
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/shadcn/ui/sidebar";
+
+export function NavSecondary({
+ items,
+ ...props
+}: {
+ items: {
+ title: string;
+ url: string;
+ icon: LucideIcon;
+ }[];
+} & React.ComponentPropsWithoutRef) {
+ const pathname = usePathname();
+
+ const isUrlActive = (itemUrl: string) => {
+ if (!pathname) return false;
+ return (
+ pathname === itemUrl ||
+ pathname.startsWith(`${itemUrl}/`) ||
+ pathname.startsWith(`${itemUrl}?`)
+ );
+ };
+
+ return (
+
+
+
+ {items.map((item) => {
+ const isActive = isUrlActive(item.url);
+
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/shadcn/ui/breadcrumb.tsx b/apps/web/src/components/shadcn/ui/breadcrumb.tsx
new file mode 100644
index 00000000..f19046ac
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "@/lib/utils/client/cn";
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = "Breadcrumb";
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = "BreadcrumbList";
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = "BreadcrumbItem";
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = "BreadcrumbLink";
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = "BreadcrumbPage";
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:h-3.5 [&>svg]:w-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/apps/web/src/components/shadcn/ui/collapsible.tsx b/apps/web/src/components/shadcn/ui/collapsible.tsx
new file mode 100644
index 00000000..cb003d17
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
+
+const Collapsible = CollapsiblePrimitive.Root;
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/apps/web/src/components/shadcn/ui/separator.tsx b/apps/web/src/components/shadcn/ui/separator.tsx
new file mode 100644
index 00000000..c9760f23
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/separator.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "@/lib/utils/client/cn";
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref,
+ ) => (
+
+ ),
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/apps/web/src/components/shadcn/ui/sheet.tsx b/apps/web/src/components/shadcn/ui/sheet.tsx
new file mode 100644
index 00000000..c4d32649
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/sheet.tsx
@@ -0,0 +1,138 @@
+"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/client/cn";
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = SheetPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, 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,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = "SheetHeader";
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = "SheetFooter";
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/apps/web/src/components/shadcn/ui/sidebar.tsx b/apps/web/src/components/shadcn/ui/sidebar.tsx
new file mode 100644
index 00000000..f4ff8527
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/sidebar.tsx
@@ -0,0 +1,799 @@
+"use client";
+
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeft } from "lucide-react";
+import { Button } from "@/components/shadcn/ui/button";
+import { Input } from "@/components/shadcn/ui/input";
+import { Separator } from "@/components/shadcn/ui/separator";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/shadcn/ui/sheet";
+import { Skeleton } from "@/components/shadcn/ui/skeleton";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/shadcn/ui/tooltip";
+import { useIsMobile } from "@/lib/hooks/use-mobile";
+import { cn } from "@/lib/utils/client/cn";
+
+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 = "3rem";
+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;
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ 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}
+
+
+
+ );
+ },
+);
+SidebarProvider.displayName = "SidebarProvider";
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ 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 */}
+
+
+
+ );
+ },
+);
+Sidebar.displayName = "Sidebar";
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarTrigger.displayName = "SidebarTrigger";
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarRail.displayName = "SidebarRail";
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInset.displayName = "SidebarInset";
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInput.displayName = "SidebarInput";
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarHeader.displayName = "SidebarHeader";
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarFooter.displayName = "SidebarFooter";
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarSeparator.displayName = "SidebarSeparator";
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarContent.displayName = "SidebarContent";
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarGroup.displayName = "SidebarGroup";
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupLabel.displayName = "SidebarGroupLabel";
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ 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 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupAction.displayName = "SidebarGroupAction";
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarGroupContent.displayName = "SidebarGroupContent";
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenu.displayName = "SidebarMenu";
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuItem.displayName = "SidebarMenuItem";
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none 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 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>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",
+ },
+ },
+);
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+ },
+);
+SidebarMenuButton.displayName = "SidebarMenuButton";
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ 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 after:md: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 &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuAction.displayName = "SidebarMenuAction";
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuBadge.displayName = "SidebarMenuBadge";
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+});
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuSub.displayName = "SidebarMenuSub";
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => );
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "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}
+ />
+ );
+});
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+
+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/apps/web/src/components/shadcn/ui/skeleton.tsx b/apps/web/src/components/shadcn/ui/skeleton.tsx
new file mode 100644
index 00000000..b611d7a7
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils/client/cn";
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/apps/web/src/components/shadcn/ui/tooltip.tsx b/apps/web/src/components/shadcn/ui/tooltip.tsx
new file mode 100644
index 00000000..60ea7e5e
--- /dev/null
+++ b/apps/web/src/components/shadcn/ui/tooltip.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils/client/cn";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/web/src/lib/constants/admin.ts b/apps/web/src/lib/constants/admin.ts
new file mode 100644
index 00000000..062fe439
--- /dev/null
+++ b/apps/web/src/lib/constants/admin.ts
@@ -0,0 +1,106 @@
+import type { LucideIcon } from "lucide-react";
+import {
+ SquareTerminal,
+ Users,
+ CalendarDays,
+ ShieldCheck,
+ ToggleLeft,
+ ScanLine,
+ Book,
+ MessageCircleQuestion,
+} from "lucide-react";
+import { PermissionType } from "@/lib/constants/permission";
+
+export type AdminNavItem = {
+ title: string;
+ url: string;
+ icon: LucideIcon;
+ isActive?: boolean;
+ permission?: PermissionType | PermissionType[];
+ items?: {
+ title: string;
+ url: string;
+ permission?: PermissionType | PermissionType[];
+ }[];
+};
+
+export type AdminSidebarData = {
+ navMain: AdminNavItem[];
+ navSecondary: {
+ title: string;
+ url: string;
+ icon: LucideIcon;
+ permission?: PermissionType | PermissionType[];
+ }[];
+};
+
+export type BreadcrumbLabels = Record;
+
+export const adminSidebarData: AdminSidebarData = {
+ navMain: [
+ {
+ title: "Overview",
+ url: "/admin",
+ icon: SquareTerminal,
+ isActive: true,
+ permission: PermissionType.ADMIN,
+ },
+ {
+ title: "Users",
+ url: "/admin/users",
+ icon: Users,
+ permission: PermissionType.VIEW_USERS,
+ },
+ {
+ title: "Events",
+ url: "/admin/events",
+ icon: CalendarDays,
+ permission: PermissionType.VIEW_EVENTS,
+ },
+ {
+ title: "Roles",
+ url: "/admin/roles",
+ icon: ShieldCheck,
+ permission: PermissionType.VIEW_ROLES,
+ },
+ {
+ title: "Toggles",
+ url: "/admin/toggles",
+ icon: ToggleLeft,
+ permission: PermissionType.MANAGE_NAVLINKS,
+ },
+ {
+ title: "Hackathon Check-in",
+ url: "/admin/check-in",
+ icon: ScanLine,
+ permission: [PermissionType.CHECK_IN, PermissionType.CREATE_SCANS],
+ },
+ ],
+ navSecondary: [
+ {
+ title: "Documentation",
+ url: "https://acmutsa.dev/hackkit",
+ icon: Book,
+ },
+ {
+ title: "Join our Discord",
+ url: "https://discord.gg/PmVFgcJ6du",
+ icon: MessageCircleQuestion,
+ },
+ ],
+};
+
+export const BreadcrumbLabels: BreadcrumbLabels = {
+ admin: "Admin",
+ "check-in": "Check In",
+ events: "Events",
+ edit: "Edit",
+ new: "New",
+ roles: "Roles",
+ scanner: "Scanner",
+ toggles: "Toggles",
+ dashboard: "Dashboard",
+ landing: "Landing",
+ registration: "Registration",
+ users: "Users",
+};
diff --git a/apps/web/src/lib/hooks/use-mobile.tsx b/apps/web/src/lib/hooks/use-mobile.tsx
new file mode 100644
index 00000000..530a3d9e
--- /dev/null
+++ b/apps/web/src/lib/hooks/use-mobile.tsx
@@ -0,0 +1,23 @@
+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/apps/web/src/lib/utils/server/admin.ts b/apps/web/src/lib/utils/server/admin.ts
index 16113606..89af0854 100644
--- a/apps/web/src/lib/utils/server/admin.ts
+++ b/apps/web/src/lib/utils/server/admin.ts
@@ -4,7 +4,7 @@ import { UserWithRole } from "db/types";
export function userHasPermission(
user: UserWithRole,
- permissions: PermissionType | [PermissionType],
+ permissions: PermissionType | PermissionType[],
): boolean {
const userPermissionMask = new PermissionMask(user.role?.permissions || 0);
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index 81e33b92..138375ed 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -58,6 +58,18 @@ module.exports = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
+ sidebar: {
+ DEFAULT: "hsl(var(--sidebar-background))",
+ foreground: "hsl(var(--sidebar-foreground))",
+ primary: "hsl(var(--sidebar-primary))",
+ "primary-foreground":
+ "hsl(var(--sidebar-primary-foreground))",
+ accent: "hsl(var(--sidebar-accent))",
+ "accent-foreground":
+ "hsl(var(--sidebar-accent-foreground))",
+ border: "hsl(var(--sidebar-border))",
+ ring: "hsl(var(--sidebar-ring))",
+ },
},
borderRadius: {
lg: "var(--radius)",
@@ -66,12 +78,20 @@ module.exports = {
},
keyframes: {
"accordion-down": {
- from: { height: 0 },
- to: { height: "var(--radix-accordion-content-height)" },
+ from: {
+ height: 0,
+ },
+ to: {
+ height: "var(--radix-accordion-content-height)",
+ },
},
"accordion-up": {
- from: { height: "var(--radix-accordion-content-height)" },
- to: { height: 0 },
+ from: {
+ height: "var(--radix-accordion-content-height)",
+ },
+ to: {
+ height: 0,
+ },
},
},
animation: {
diff --git a/packages/config/hackkit.config.ts b/packages/config/hackkit.config.ts
index 4d48e1ae..87c40fb0 100644
--- a/packages/config/hackkit.config.ts
+++ b/packages/config/hackkit.config.ts
@@ -115,14 +115,6 @@ const c = {
Schedule: "/dash/schedule",
"Event Pass": "/dash/pass",
},
- admin: {
- Overview: "/admin",
- Users: "/admin/users",
- Events: "/admin/events",
- Roles: "/admin/roles",
- Toggles: "/admin/toggles",
- "Hackathon Check-in": "/admin/check-in",
- },
},
eventTypes: {
Meal: "#FFC107",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0fdad4fa..50b464c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -84,6 +84,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-collapsible':
+ specifier: ^1.1.12
+ version: 1.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@@ -99,12 +102,18 @@ importers:
'@radix-ui/react-select':
specifier: ^1.2.2
version: 1.2.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.8
+ version: 1.2.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@t3-oss/env-nextjs':
specifier: ^0.10.1
version: 0.10.1(typescript@5.5.3)(zod@3.25.67)
@@ -2213,6 +2222,10 @@ packages:
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
dev: false
+ /@radix-ui/primitive@1.1.3:
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+ dev: false
+
/@radix-ui/react-accordion@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==}
peerDependencies:
@@ -2288,6 +2301,26 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ 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
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-avatar@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-9ToF7YNex3Ste45LrAeTlKtONI9yVRt/zOS158iilIkW5K/Apeyb/TUQlcEFTEFvWr8Kzdi2ZYrm1/suiXPajQ==}
peerDependencies:
@@ -2368,6 +2401,33 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
+ 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
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies:
@@ -2419,6 +2479,19 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies:
@@ -2446,6 +2519,19 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-context@1.1.2(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-dialog@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==}
peerDependencies:
@@ -2635,6 +2721,30 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ 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
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==}
peerDependencies:
@@ -2786,6 +2896,20 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-id@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-label@2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
peerDependencies:
@@ -2910,6 +3034,35 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ 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
+ dependencies:
+ '@floating-ui/react-dom': 2.0.1(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/rect': 1.1.1
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-portal@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==}
peerDependencies:
@@ -2973,6 +3126,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ 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
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
@@ -3016,6 +3190,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ 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
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
@@ -3057,6 +3252,46 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ 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
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
+ 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
+ dependencies:
+ '@radix-ui/react-slot': 1.2.4(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies:
@@ -3127,6 +3362,26 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
dev: false
+ /@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
+ 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
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@@ -3156,6 +3411,34 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-slot@1.2.3(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-slot@1.2.4(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
peerDependencies:
@@ -3183,6 +3466,37 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ 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
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
@@ -3210,6 +3524,19 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies:
@@ -3239,6 +3566,35 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
@@ -3268,6 +3624,20 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
peerDependencies:
@@ -3295,6 +3665,19 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-previous@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
peerDependencies:
@@ -3324,6 +3707,20 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-rect@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-size@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==}
peerDependencies:
@@ -3339,6 +3736,20 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-size@1.1.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
peerDependencies:
@@ -3360,12 +3771,36 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ 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
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
'@babel/runtime': 7.24.8
dev: false
+ /@radix-ui/rect@1.1.1:
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ dev: false
+
/@react-aria/breadcrumbs@3.5.13(react@18.3.1):
resolution: {integrity: sha512-G1Gqf/P6kVdfs94ovwP18fTWuIxadIQgHsXS08JEVcFVYMjb9YjqnEBaohUxD1tq2WldMbYw53ahQblT4NTG+g==}
peerDependencies: