Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/web/src/actions/admin/scanner-admin-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const createScan = volunteerAction
eventID: eventID,
});
}
return { success: true };
return { success: true, name: user.firstName };
},
);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/admin/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { columns } from "@/components/events/shared/EventColumns";
import { Button } from "@/components/shadcn/ui/button";
import { PlusCircle } from "lucide-react";
import Link from "next/link";
import { getAllEvents, getUser } from "db/functions";
import { getAllEventsWithScans, getUser } from "db/functions";
import { auth } from "@clerk/nextjs/server";
import FullScreenMessage from "@/components/shared/FullScreenMessage";
import { isUserAdmin } from "@/lib/utils/server/admin";
Expand All @@ -26,7 +26,7 @@ export default async function Page() {
);
}

const events = await getAllEvents();
const events = await getAllEventsWithScans();
const isUserAuthorized = isUserAdmin(userData);
return (
<div className="mx-auto max-w-7xl px-5 pt-44">
Expand Down
93 changes: 51 additions & 42 deletions apps/web/src/app/admin/users/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
ProfileInfo,
} from "@/components/admin/users/ServerSections";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from "@/components/shadcn/ui/dropdown-menu";
import { auth } from "@clerk/nextjs/server";
import { notFound } from "next/navigation";
Expand Down Expand Up @@ -49,7 +49,7 @@ export default async function Page({ params }: { params: { slug: string } }) {
{/* <p className="text-sm text-muted-foreground">{users.length} Total Users</p> */}
</div>
</div>
<div className="col-span-2 hidden md:flex items-center justify-end gap-2 ">
<div className="col-span-2 hidden items-center justify-end gap-2 md:flex">
<Link href={`/@${user.hackerTag}`} target="_blank">
<Button variant={"outline"}>Hacker Profile</Button>
</Link>
Expand All @@ -71,43 +71,52 @@ export default async function Page({ params }: { params: { slug: string } }) {
/>
)}
</div>
<div className="col-span-2 flex md:hidden items-center justify-end pr-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={"outline"} >
Admin Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[160px]">
<DropdownMenuItem className="justify-center">
<Link href={`/@${user.hackerTag}`} target="_blank">
<Button variant={"outline"}>Hacker Profile</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-center">
<Link href={`mailto:${user.email}`}>
<Button variant={"outline"}>Email Hacker</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="px-2 py-1.5 text-sm text-center hover:bg-accent rounded-sm cursor-pointer">
<UpdateRoleDialog
name={`${user.firstName} ${user.lastName}`}
canMakeAdmins={admin.role === "super_admin"}
currPermision={user.role}
userID={user.clerkID}
/>
</div>
<div className="col-span-2 flex items-center justify-end pr-4 md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={"outline"}>Admin Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="min-w-[160px]"
>
<DropdownMenuItem className="justify-center">
<Link
href={`/@${user.hackerTag}`}
target="_blank"
>
<Button variant={"outline"}>
Hacker Profile
</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-center">
<Link href={`mailto:${user.email}`}>
<Button variant={"outline"}>
Email Hacker
</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="cursor-pointer rounded-sm px-2 py-1.5 text-center text-sm hover:bg-accent">
<UpdateRoleDialog
name={`${user.firstName} ${user.lastName}`}
canMakeAdmins={admin.role === "super_admin"}
currPermision={user.role}
userID={user.clerkID}
/>
</div>

{(c.featureFlags.core.requireUsersApproval as boolean) && (
<ApproveUserButton
userIDToUpdate={user.clerkID}
currentApproval={user.isApproved}
/>
)}
</DropdownMenuContent>
</DropdownMenu>
{(c.featureFlags.core
.requireUsersApproval as boolean) && (
<ApproveUserButton
userIDToUpdate={user.clerkID}
currentApproval={user.isApproved}
/>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="mt-20 grid min-h-[500px] w-full grid-cols-3">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/dash/pass/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function EventPass({ qrPayload, user, clerk, guild }: EventPassProps) {
c.startDate,
"h:mma, MMM d, yyyy",
)}`}</p>
<p className="font-mono text-xs text-right">
<p className="text-right font-mono text-xs">
{c.prettyLocation}
</p>
</div>
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@
}

@keyframes pulseDot {

0%,
100% {
transform: scale(1);
Expand All @@ -148,4 +147,4 @@
50% {
transform: scale(1.1);
}
}
}
43 changes: 41 additions & 2 deletions apps/web/src/components/admin/scanner/PassScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Button } from "@/components/shadcn/ui/button";
import Link from "next/link";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { ACTION_VALIDATION_ERRORS } from "@/lib/constants";

/*

Expand All @@ -46,7 +47,36 @@ export default function PassScanner({
scanUser,
}: PassScannerProps) {
const [scanLoading, setScanLoading] = useState(false);
const { execute: runScanAction } = useAction(createScan, {});
const { execute: runScanAction } = useAction(createScan, {
onExecute: () => {
toast.loading("Processing scan...");
},
onSettled: () => {
toast.dismiss();
},
onError: (error) => {
if (error.error.validationErrors?._errors) {
const errors = error.error.validationErrors?._errors;
if (
errors.includes(
ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NO_USER_ID,
) ||
errors.includes(
ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN,
)
) {
toast.error(
"You do not have permission to scan users. Please ask a super admin for assistance.",
);
return;
}
}
toast.error("Error scanning user. Please try again.");
},
onSuccess: (res) => {
toast.success(`${res.data?.name || "User"} scanned successfully!`);
},
});

useEffect(() => {
if (hasScanned) {
Expand All @@ -64,6 +94,7 @@ export default function PassScanner({
const guild =
Object.keys(c.groups)[scanUser?.hackerData.group || 0] ?? "None";
const role = scanUser?.role ? scanUser?.role : "Not Found";
const dietaryRestrictions = scanUser?.dietRestrictions || [];

function handleScanCreate() {
const params = new URLSearchParams(searchParams.toString());
Expand All @@ -90,7 +121,6 @@ export default function PassScanner({
});
}

toast.success("Successfully Scanned User In");
router.replace(`${path}`);
}

Expand Down Expand Up @@ -186,6 +216,15 @@ export default function PassScanner({
</span>{" "}
{guild}
</h2>
{dietaryRestrictions?.length > 0 && (
<h2>
<span className="font-bold underline">
Dietary Restrictions:
</span>{" "}
{dietaryRestrictions.join(", ") ||
"None"}
</h2>
)}
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
Expand Down
11 changes: 7 additions & 4 deletions apps/web/src/components/events/shared/EventColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@ import {
} from "@/components/shadcn/ui/alert-dialog";
import { Badge } from "@/components/shadcn/ui/badge";
import c from "config";
import { eventTableValidatorType } from "@/lib/types/events";
import { EventsWithScansType } from "@/lib/types/events";
import { useState } from "react";
import { MoreHorizontal } from "lucide-react";
import { useRouter } from "next/navigation";
import { useAction } from "next-safe-action/hooks";
import { deleteEventAction } from "@/actions/admin/event-actions";
import { toast } from "sonner";
import { LoaderCircle } from "lucide-react";
import { error } from "console";

type EventRow = eventTableValidatorType & { isUserAdmin: boolean };
type EventRow = EventsWithScansType & { isUserAdmin: boolean };

export const columns: ColumnDef<EventRow>[] = [
{
Expand Down Expand Up @@ -88,6 +86,11 @@ export const columns: ColumnDef<EventRow>[] = [
</span>
),
},
{
accessorKey: "totalCheckins",
header: "Total Scans",
cell: ({ row }) => <span>{row.original.totalScans || 0}</span>,
},
{
accessorKey: "actions",
header: "Actions",
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const HACKER_REGISTRATION_STORAGE_KEY = `${c.hackathonName}_${c.itteratio
export const HACKER_REGISTRATION_RESUME_STORAGE_KEY =
"hackerRegistrationResume";
export const NOT_LOCAL_SCHOOL = "NOT_LOCAL_SCHOOL";
export const ACTION_VALIDATION_ERRORS = {
UNAUTHORIZED_NO_USER_ID: "Unauthorized (No User ID)",
UNAUTHORIZED_NOT_ADMIN: "Unauthorized (Not Admin)",
};
7 changes: 4 additions & 3 deletions apps/web/src/lib/safe-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { auth } from "@clerk/nextjs/server";
import { getUser } from "db/functions";
import { z } from "zod";
import { isUserAdmin } from "./utils/server/admin";
import { ACTION_VALIDATION_ERRORS } from "@/lib/constants";

export const publicAction = createSafeActionClient();

Expand All @@ -15,7 +16,7 @@ export const authenticatedAction = publicAction.use(
const { userId } = await auth();
if (!userId)
returnValidationErrors(z.null(), {
_errors: ["Unauthorized (No User ID)"],
_errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NO_USER_ID],
});
// TODO: add check for registration
return next({ ctx: { userId } });
Expand All @@ -30,7 +31,7 @@ export const volunteerAction = authenticatedAction.use(
!["admin", "super_admin", "volunteer"].includes(user.role)
) {
returnValidationErrors(z.null(), {
_errors: ["Unauthorized (Not Admin)"],
_errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN],
});
}
return next({ ctx: { user, ...ctx } });
Expand All @@ -41,7 +42,7 @@ export const adminAction = authenticatedAction.use(async ({ next, ctx }) => {
const user = await getUser(ctx.userId);
if (!user || !isUserAdmin(user)) {
returnValidationErrors(z.null(), {
_errors: ["Unauthorized (Not Admin)"],
_errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN],
});
}
return next({ ctx: { user, ...ctx } });
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/lib/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ export type EventTypeEnum = [
...Array<keyof typeof c.eventTypes>,
];

export type eventTableValidatorType = Pick<
export type EventTableValidatorType = Pick<
z.infer<typeof eventDataTableValidator>,
"title" | "location" | "startTime" | "endTime" | "id" | "type"
>;

export type EventsWithScansType = EventTableValidatorType & {
totalScans: string | null;
};

export interface NewEventFormProps {
defaultDate: Date;
}

export interface getAllEventsOptions {
export interface GetAllEventsOptions {
descending?: boolean;
}
26 changes: 21 additions & 5 deletions packages/db/functions/events.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { db, asc, desc, eq } from "..";
import { db, asc, desc, eq, getTableColumns, sum } from "..";
import {
eventEditType,
eventInsertType,
getAllEventsOptions,
GetAllEventsOptions,
} from "../../../apps/web/src/lib/types/events";
import { events } from "../schema";
import { events, scans } from "../schema";

export function createNewEvent(event: eventInsertType) {
export async function createNewEvent(event: eventInsertType) {
return db
.insert(events)
.values({
Expand All @@ -17,7 +17,7 @@ export function createNewEvent(event: eventInsertType) {
});
}

export function getAllEvents(options?: getAllEventsOptions) {
export async function getAllEvents(options?: GetAllEventsOptions) {
const orderByClause = options?.descending
? [desc(events.startTime)]
: [asc(events.startTime)];
Expand All @@ -27,6 +27,22 @@ export function getAllEvents(options?: getAllEventsOptions) {
});
}

export async function getAllEventsWithScans(options?: GetAllEventsOptions) {
const orderByClause = options?.descending
? desc(events.startTime)
: asc(events.startTime);

return db
.select({
...getTableColumns(events),
totalScans: sum(scans.count),
})
.from(events)
.leftJoin(scans, eq(events.id, scans.eventID))
.groupBy(events.id, scans.eventID)
.orderBy(orderByClause);
}

export async function getEventById(eventId: number) {
return db.query.events.findFirst({ where: eq(events.id, eventId) });
}
Expand Down