Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/events/edit/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default async function EditEventPage({
}

return (
<div className="mx-auto max-w-3xl pt-32">
<div className="mx-auto max-w-3xl">
<div className="grid grid-cols-2">
<h1 className="text-3xl font-bold tracking-tight">
Edit Event
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/events/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function Page() {
const defaultDate = new Date();

return (
<div className="mx-auto max-w-3xl pt-32">
<div className="mx-auto max-w-3xl">
<div className="grid grid-cols-2">
<h1 className="text-3xl font-bold tracking-tight">New Event</h1>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default async function Page() {
PermissionType.CREATE_EVENTS,
);
return (
<div className="mx-auto max-w-7xl px-5 pt-44">
<div className="mx-auto max-w-7xl px-5">
<div className="mb-5 grid w-full grid-cols-2">
<div className="flex items-center">
<div>
Expand Down
114 changes: 34 additions & 80 deletions apps/web/src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,88 +21,40 @@ export default async function AdminLayout({ children }: AdminLayoutProps) {
const user = await getCurrentUser();

if (!isUserAdmin(user)) {
console.log("Denying admin access to user", user);
return (
<FullScreenMessage
title="Access Denied"
message="You are not an admin. If you belive this is a mistake, please contact a administrator."
message="You are not an admin. If you believe this is a mistake, please contact an administrator."
/>
);
}

return (
<>
<ClientToast duration={2500} position="top-right" />
<div className="fixed z-20 grid h-16 w-full grid-cols-2 bg-nav px-5">
<div className="flex items-center gap-x-4">
<Link href={"/"} className="mr-5 flex items-center gap-x-2">
<Image
src={c.icon.svg}
alt={c.hackathonName + " Logo"}
width={32}
height={32}
/>
<div className="h-[45%] w-[2px] rotate-[25deg] bg-muted-foreground" />
<h2 className="font-bold tracking-tight">Admin</h2>
</Link>
</div>
<div className="hidden items-center justify-end gap-x-4 md:flex">
<Link href={"/"}>
<Button
variant={"outline"}
className="bg-nav hover:bg-background"
>
Home
</Button>
</Link>
<Link href={c.links.guide}>
<Button
variant={"outline"}
className="bg-nav hover:bg-background"
>
Survival Guide
</Button>
</Link>
<Link href={c.links.discord}>
<Button
variant={"outline"}
className="bg-nav hover:bg-background"
>
Discord
</Button>
</Link>
<ProfileButton />
</div>
<div className="flex items-center justify-end gap-x-4 md:hidden"></div>
</div>
<div className="fixed z-20 mt-16 flex h-12 w-full border-b border-b-border bg-nav px-5">
{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 <DashNavItem key={name} name={name} path={path} />;
})}
</div>
<Suspense fallback={<p>Loading...</p>}>{children}</Suspense>
<SidebarProvider>
<AdminSidebar user={user} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mr-2 h-4"
/>
<AdminBreadcrumbs />
</div>
<div className="ml-auto flex pr-4">
<NavUserProfile user={user} />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-8">
<Suspense fallback={<p>Loading...</p>}>
{children}
</Suspense>
</div>
</SidebarInset>
</SidebarProvider>
</>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default async function Page() {
const timezone = getClientTimeZone(c.hackathonTimezone);

return (
<div className="mx-auto h-16 w-full max-w-7xl pt-44">
<div className="mx-auto h-16 w-full max-w-7xl">
<div className="w-full px-2">
<h2 className="text-xl font-bold">Welcome,</h2>
<h1 className="text-5xl font-black text-hackathon">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/roles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function Page() {

// pass serializable data to client
return (
<div className="mx-auto max-w-7xl px-5 pt-40">
<div className="mx-auto w-full max-w-7xl px-5">
<h1 className="mb-4 text-2xl font-bold">Roles</h1>
<Suspense
fallback={
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/toggles/landing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getAllNavItems } from "@/lib/utils/server/redis";
export default async function Page() {
const nav = await getAllNavItems();
return (
<div>
<div className="w-full">
<div className="flex items-center justify-start">
<h2 className="text-3xl font-bold tracking-tight">
Navbar Items
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/toggles/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function Layout({ children }: ToggleLayoutProps) {
return notFound();

return (
<div className="mx-auto grid max-w-5xl grid-cols-5 gap-x-3 pt-44">
<div className="mx-auto grid w-full max-w-5xl grid-cols-5 gap-x-3">
<div className="min-h-screen">
<ToggleItem name="Toggles" path="/admin/toggles" />
<ToggleItem name="Landing Page" path="/admin/toggles/landing" />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/users/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default async function Page({ params }: { params: { slug: string } }) {
});

return (
<main className="mx-auto max-w-5xl pt-44">
<main className="mx-auto max-w-5xl">
{!!banInstance && (
<div className="absolute left-0 top-28 w-screen bg-destructive p-2 text-center">
<strong>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function Page() {
const userData = await getAllUsers();

return (
<div className="mx-auto max-w-7xl px-5 pt-40">
<div className="mx-auto max-w-7xl px-5">
<div className="mb-5 grid w-full grid-cols-2">
<div className="flex items-center">
<div>
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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%;
}
}

Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/admin/scanner/CheckinScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ export default function CheckinScanner({

return (
<>
<div className="flex h-dvh flex-col items-center justify-center pt-32">
<div className="flex w-screen flex-col items-center justify-center gap-5">
<div className="mx-auto aspect-square w-screen max-w-[500px] overflow-hidden">
<div className="flex h-full flex-col items-center justify-center pt-32">
<div className="flex w-full flex-col items-center justify-center gap-5">
<div className="mx-auto aspect-square w-full max-w-[500px] overflow-hidden">
<Scanner
onScan={(result) => {
const params = new URLSearchParams(
Expand All @@ -156,7 +156,7 @@ export default function CheckinScanner({
onError={(error) => console.log(error)}
styles={{
container: {
width: "100vw",
width: "100%",
maxWidth: "500px",
margin: "0",
},
Expand Down
70 changes: 70 additions & 0 deletions apps/web/src/components/admin/shared/AdminBreadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Breadcrumb>
<BreadcrumbList>
{allSegments.map((segment, index) => {
const href =
"/" + allSegments.slice(0, index + 1).join("/");
const isLast = index === allSegments.length - 1;

return (
<React.Fragment key={segment}>
<BreadcrumbItem
className={!isLast ? "hidden md:block" : ""}
>
{isLast ? (
<BreadcrumbPage className="text-base">
{formatSegment(segment)}
</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={href} className="text-base">
{formatSegment(segment)}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>

{!isLast && (
<BreadcrumbSeparator className="hidden md:block" />
)}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
}
Loading
Loading