From 9e4282a8fd294d3e235d7baa459a87f5e53d416c Mon Sep 17 00:00:00 2001 From: Rufat Niftaliyev Date: Mon, 30 Mar 2026 01:27:18 -0500 Subject: [PATCH 1/4] Sidebar ready --- apps/web/package.json | 3 + .../src/app/admin/events/edit/[slug]/page.tsx | 2 +- apps/web/src/app/admin/events/new/page.tsx | 2 +- apps/web/src/app/admin/events/page.tsx | 2 +- apps/web/src/app/admin/layout.tsx | 135 ++- apps/web/src/app/admin/page.tsx | 2 +- apps/web/src/app/admin/roles/page.tsx | 2 +- .../src/app/admin/toggles/landing/page.tsx | 2 +- apps/web/src/app/admin/toggles/layout.tsx | 2 +- apps/web/src/app/admin/users/[slug]/page.tsx | 2 +- apps/web/src/app/admin/users/page.tsx | 2 +- apps/web/src/app/dashboard/page.tsx | 52 ++ apps/web/src/app/globals.css | 24 + .../web/src/components/shadcn/app-sidebar.tsx | 76 ++ apps/web/src/components/shadcn/nav-main.tsx | 91 ++ .../src/components/shadcn/nav-projects.tsx | 89 ++ .../src/components/shadcn/nav-secondary.tsx | 44 + apps/web/src/components/shadcn/nav-user.tsx | 117 +++ .../src/components/shadcn/ui/breadcrumb.tsx | 115 +++ .../src/components/shadcn/ui/collapsible.tsx | 11 + .../src/components/shadcn/ui/separator.tsx | 31 + apps/web/src/components/shadcn/ui/sheet.tsx | 140 +++ apps/web/src/components/shadcn/ui/sidebar.tsx | 799 ++++++++++++++++++ .../web/src/components/shadcn/ui/skeleton.tsx | 15 + apps/web/src/components/shadcn/ui/tooltip.tsx | 30 + apps/web/src/lib/constants/admin.ts | 89 ++ apps/web/src/lib/hooks/use-mobile.tsx | 19 + apps/web/src/lib/utils/server/admin.ts | 2 +- apps/web/tailwind.config.js | 165 ++-- packages/config/hackkit.config.ts | 8 - pnpm-lock.yaml | 435 ++++++++++ 31 files changed, 2338 insertions(+), 170 deletions(-) create mode 100644 apps/web/src/app/dashboard/page.tsx create mode 100644 apps/web/src/components/shadcn/app-sidebar.tsx create mode 100644 apps/web/src/components/shadcn/nav-main.tsx create mode 100644 apps/web/src/components/shadcn/nav-projects.tsx create mode 100644 apps/web/src/components/shadcn/nav-secondary.tsx create mode 100644 apps/web/src/components/shadcn/nav-user.tsx create mode 100644 apps/web/src/components/shadcn/ui/breadcrumb.tsx create mode 100644 apps/web/src/components/shadcn/ui/collapsible.tsx create mode 100644 apps/web/src/components/shadcn/ui/separator.tsx create mode 100644 apps/web/src/components/shadcn/ui/sheet.tsx create mode 100644 apps/web/src/components/shadcn/ui/sidebar.tsx create mode 100644 apps/web/src/components/shadcn/ui/skeleton.tsx create mode 100644 apps/web/src/components/shadcn/ui/tooltip.tsx create mode 100644 apps/web/src/lib/constants/admin.ts create mode 100644 apps/web/src/lib/hooks/use-mobile.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 0bef44d7..55eed01f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,13 +20,16 @@ "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-select": "^1.2.2", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tooltip": "^1.2.8", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.19.3", diff --git a/apps/web/src/app/admin/events/edit/[slug]/page.tsx b/apps/web/src/app/admin/events/edit/[slug]/page.tsx index aa2c4346..95e3a7dd 100644 --- a/apps/web/src/app/admin/events/edit/[slug]/page.tsx +++ b/apps/web/src/app/admin/events/edit/[slug]/page.tsx @@ -28,7 +28,7 @@ export default async function EditEventPage({ } return ( -
+

Edit Event diff --git a/apps/web/src/app/admin/events/new/page.tsx b/apps/web/src/app/admin/events/new/page.tsx index 2de0a160..95ac9597 100644 --- a/apps/web/src/app/admin/events/new/page.tsx +++ b/apps/web/src/app/admin/events/new/page.tsx @@ -12,7 +12,7 @@ export default async function Page() { const defaultDate = new Date(); return ( -
+

New Event

diff --git a/apps/web/src/app/admin/events/page.tsx b/apps/web/src/app/admin/events/page.tsx index 1120e5cc..0e8d25f9 100644 --- a/apps/web/src/app/admin/events/page.tsx +++ b/apps/web/src/app/admin/events/page.tsx @@ -25,7 +25,7 @@ export default async function Page() { PermissionType.CREATE_EVENTS, ); return ( -
+
diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx index 6ce3f59a..27c129b8 100644 --- a/apps/web/src/app/admin/layout.tsx +++ b/apps/web/src/app/admin/layout.tsx @@ -1,15 +1,26 @@ -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 { AppSidebar } from "@/components/shadcn/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/shadcn/ui/breadcrumb"; +import { Separator } from "@/components/shadcn/ui/separator"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/shadcn/ui/sidebar"; +import ProfileButton from "@/components/shared/ProfileButton"; +import { NavUser } from "@/components/shadcn/nav-user"; +import { adminSidebarData } from "@/lib/constants/admin"; interface AdminLayoutProps { children: React.ReactNode; @@ -19,7 +30,6 @@ export default async function AdminLayout({ children }: AdminLayoutProps) { const user = await getCurrentUser(); if (!isUserAdmin(user)) { - console.log("Denying admin access to user", user); return ( -
-
- - {c.hackathonName -
-

Admin

- -
-
- - - - - - - - - - -
-
-
-
- {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}
+ + + +
+
+ + + + + + + Building Your Application + + + + + + Data Fetching + + + + +
+
+ +
+
+
+ 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/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx new file mode 100644 index 00000000..eafa278b --- /dev/null +++ b/apps/web/src/app/dashboard/page.tsx @@ -0,0 +1,52 @@ +import { AppSidebar } from "@/components/shadcn/app-sidebar" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/shadcn/ui/breadcrumb" +import { Separator } from "@/components/shadcn/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/shadcn/ui/sidebar" + +export default function Page() { + return ( + + + +
+
+ + + + + + + Building Your Application + + + + + Data Fetching + + + +
+
+
+
+
+
+
+
+
+
+ + + ) +} 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/shadcn/app-sidebar.tsx b/apps/web/src/components/shadcn/app-sidebar.tsx new file mode 100644 index 00000000..097f7ba2 --- /dev/null +++ b/apps/web/src/components/shadcn/app-sidebar.tsx @@ -0,0 +1,76 @@ +"use client"; + +import * as React from "react"; +import { NavMain } from "@/components/shadcn/nav-main"; +import { NavSecondary } from "@/components/shadcn/nav-secondary"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/shadcn/ui/sidebar"; +import c from "config"; +import Image from "next/image"; +import Link from "next/link"; +import { UserWithRole } from "db/types"; +import { userHasPermission } from "@/lib/utils/server/admin"; +import { adminSidebarData as data } from "@/lib/constants/admin"; + +export async function AppSidebar({ + user, + ...props +}: React.ComponentProps & { + user: UserWithRole; +}) { + const mainItems = data.navMain.filter((item) => + item.permission ? userHasPermission(user, item.permission) : true, + ); + const secondaryItems = data.navSecondary.filter((item) => + item.permission ? userHasPermission(user, item.permission) : true, + ); + + return ( + + + + + +
+ + {c.hackathonName +
+

+ Admin +

+ +
+ + + + + + + {/* */} + + + +
+ + Powered by HackKit + +
+
+ + ); +} diff --git a/apps/web/src/components/shadcn/nav-main.tsx b/apps/web/src/components/shadcn/nav-main.tsx new file mode 100644 index 00000000..ee1dc56a --- /dev/null +++ b/apps/web/src/components/shadcn/nav-main.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { ChevronRight, type LucideIcon } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/shadcn/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + 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; + }[]; + }[]; +}) { + return ( + + + {items.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + + + + + {item.items?.map((subItem) => ( + + + + + {subItem.title} + + + + + ))} + + + + ) : null} + + + ))} + + + ); +} diff --git a/apps/web/src/components/shadcn/nav-projects.tsx b/apps/web/src/components/shadcn/nav-projects.tsx new file mode 100644 index 00000000..60a04a73 --- /dev/null +++ b/apps/web/src/components/shadcn/nav-projects.tsx @@ -0,0 +1,89 @@ +"use client" + +import { + Folder, + MoreHorizontal, + Share, + Trash2, + type LucideIcon, +} from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/shadcn/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/shadcn/ui/sidebar" + +export function NavProjects({ + projects, +}: { + projects: { + name: string + url: string + icon: LucideIcon + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ) +} diff --git a/apps/web/src/components/shadcn/nav-secondary.tsx b/apps/web/src/components/shadcn/nav-secondary.tsx new file mode 100644 index 00000000..5abdda98 --- /dev/null +++ b/apps/web/src/components/shadcn/nav-secondary.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; + +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) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/apps/web/src/components/shadcn/nav-user.tsx b/apps/web/src/components/shadcn/nav-user.tsx new file mode 100644 index 00000000..c346eb55 --- /dev/null +++ b/apps/web/src/components/shadcn/nav-user.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { ChevronsUpDown } from "lucide-react"; + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/shadcn/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/shadcn/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} 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 "../Restricted"; +import { PermissionType } from "@/lib/constants/permission"; + +export function NavUser({ user }: { user: UserWithRole }) { + const { isMobile } = useSidebar(); + + 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/shadcn/ui/breadcrumb.tsx b/apps/web/src/components/shadcn/ui/breadcrumb.tsx new file mode 100644 index 00000000..c4e073d2 --- /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) =>