From 638fd5738a7b40da9f10013aaed19d4d64003f3b Mon Sep 17 00:00:00 2001 From: Christian Walker <76548772+christianhelp@users.noreply.github.com> Date: Thu, 24 Oct 2024 00:34:03 -0500 Subject: [PATCH] Fix Hackathon Check-In Scanner (#130) --- .../actions/admin/scanner-admin-actions.ts | 22 +- apps/web/src/app/admin/check-in/page.tsx | 4 +- .../admin/scanner/CheckinScanner.tsx | 203 +++++++++--------- .../src/components/shared/ProfileButton.tsx | 1 + apps/web/src/lib/constants/index.ts | 1 + packages/config/hackkit.config.ts | 2 +- 6 files changed, 128 insertions(+), 105 deletions(-) diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts index fb5d46a2..af21fd0e 100644 --- a/apps/web/src/actions/admin/scanner-admin-actions.ts +++ b/apps/web/src/actions/admin/scanner-admin-actions.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { db, sql } from "db"; import { scans, userCommonData } from "db/schema"; import { eq, and } from "db/drizzle"; + export const createScan = adminAction .schema( z.object({ @@ -65,12 +66,23 @@ export const getScan = adminAction }, ); -export const checkInUser = adminAction - .schema(z.string()) - .action(async ({ parsedInput: user }) => { +// Schema will be moved over when rewrite of the other scanner happens +const checkInUserSchema = z.object({ + userID: z.string(), + QRTimestamp: z + .number() + .positive() + .refine((timestamp) => { + return Date.now() - timestamp < 5 * 60 * 1000; + }, "QR Code has expired. Please tell user refresh the QR Code"), +}); + +export const checkInUserToHackathon = adminAction + .schema(checkInUserSchema) + .action(async ({ parsedInput: { userID } }) => { // Set checkinTimestamp - return await db + await db .update(userCommonData) .set({ checkinTimestamp: sql`now()` }) - .where(eq(userCommonData.clerkID, user)); + .where(eq(userCommonData.clerkID, userID)); }); diff --git a/apps/web/src/app/admin/check-in/page.tsx b/apps/web/src/app/admin/check-in/page.tsx index 0dea1ab8..af117b82 100644 --- a/apps/web/src/app/admin/check-in/page.tsx +++ b/apps/web/src/app/admin/check-in/page.tsx @@ -19,7 +19,8 @@ export default async function Page({ ); const scanUser = await getUser(searchParams.user); - if (!scanUser) + console.log(scanUser); + if (!scanUser) { return (
); + } return (
diff --git a/apps/web/src/components/admin/scanner/CheckinScanner.tsx b/apps/web/src/components/admin/scanner/CheckinScanner.tsx index d13c2be4..525ad748 100644 --- a/apps/web/src/components/admin/scanner/CheckinScanner.tsx +++ b/apps/web/src/components/admin/scanner/CheckinScanner.tsx @@ -3,11 +3,11 @@ import { useState, useEffect } from "react"; import { QrScanner } from "@yudiel/react-qr-scanner"; import superjson from "superjson"; -import { checkInUser } from "@/actions/admin/scanner-admin-actions"; -import { useAction } from "next-safe-action/hooks"; +import { checkInUserToHackathon } from "@/actions/admin/scanner-admin-actions"; import { type QRDataInterface } from "@/lib/utils/shared/qr"; import type { User } from "db/types"; - +import clsx from "clsx"; +import { useAction } from "next-safe-action/hooks"; import { Drawer, DrawerContent, @@ -19,16 +19,8 @@ import { import { Button } from "@/components/shadcn/ui/button"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { toast } from "sonner"; - -/* - -Pass Scanner Props: - -eventName: name of the event that the user is scanning into -hasScanned: if the state has eventered one in which a QR has been scanned (whether that scan has scanned before or not) -scan: the scan object that has been scanned. If they have not scanned before scan will be null leading to a new record or if they have then it will incriment the scan count. - -*/ +import { FIVE_MINUTES_IN_MILLISECONDS } from "@/lib/constants"; +import { ValidationErrors } from "next-safe-action"; interface CheckinScannerProps { hasScanned: boolean; @@ -43,9 +35,9 @@ export default function CheckinScanner({ scanUser, hasRSVP, }: CheckinScannerProps) { + console.log("scanner props is: ", hasScanned, checkedIn, scanUser, hasRSVP); + const [scanLoading, setScanLoading] = useState(false); - // const { execute: runScanAction } = useAction(checkInUser, {}); - const [proceed, setProceed] = useState(hasRSVP); useEffect(() => { if (hasScanned) { setScanLoading(false); @@ -56,22 +48,83 @@ export default function CheckinScanner({ const path = usePathname(); const router = useRouter(); + function handleUseActionFeedback(hasErrored = false, message = "") { + console.log("called"); + toast.dismiss(); + hasErrored + ? toast.error(message || "Failed to Check User Into Hackathon") + : toast.success( + message || "Successfully Checked User Into Hackathon!", + ); + router.replace(`${path}`); + } + + const { execute: runCheckInUserToHackathon } = useAction( + checkInUserToHackathon, + { + onSuccess: () => { + handleUseActionFeedback(); + }, + onError: ({ error, input }) => { + console.log("error is: ", error); + console.log("input is: ", input); + if (error.validationErrors?.QRTimestamp?._errors) { + handleUseActionFeedback( + true, + error.validationErrors.QRTimestamp._errors[0], + ); + } else { + handleUseActionFeedback(true); + } + }, + }, + ); + function handleScanCreate() { const params = new URLSearchParams(searchParams.toString()); const timestamp = parseInt(params.get("createdAt") as string); + + if (!scanUser) { + return alert("User Not Found"); + } + if (isNaN(timestamp)) { return alert("Invalid QR Code Data (Field: createdAt)"); } + if (Date.now() - timestamp > FIVE_MINUTES_IN_MILLISECONDS) { + return alert( + "QR Code has expired. Please tell user to refresh the QR Code", + ); + } + if (checkedIn) { return alert("User Already Checked in!"); } else { - // TODO: make this a little more typesafe - checkInUser(scanUser?.clerkID!); + toast.loading("Checking User In"); + runCheckInUserToHackathon({ + userID: scanUser.clerkID, + QRTimestamp: timestamp, + }); } - toast.success("Successfully Scanned User In"); - router.replace(`${path}`); + router.replace(path); } + const drawerTitle = checkedIn + ? "User Already Checked In" + : !hasRSVP + ? "Warning!" + : "New Scan"; + const drawerDescription = checkedIn + ? "If this is a mistake, please talk to an admin" + : !hasRSVP + ? `${scanUser?.firstName} ${scanUser?.lastName} Is not RSVP'd` + : `New scan for ${scanUser?.firstName} ${scanUser?.lastName}`; + const drawerFooterButtonText = checkedIn + ? "Close" + : !hasRSVP + ? "Check In Anyways" + : "Scan User In"; + return ( <>
@@ -108,11 +161,6 @@ export default function CheckinScanner({ }} />
- {/*
- - - -
*/}
Loading Scan... - {/* */} - - - - ) : ( - <> - - New Scan - - - New scan for{" "} - {scanUser?.firstName}{" "} - {scanUser?.lastName} - - - )} - - )} + + {drawerTitle} + - {proceed ? ( - <> - - {!checkedIn && ( - - )} - - - - ) : ( - <> - )} + + {drawerDescription} + + + {!hasRSVP && !checkedIn && ( +
+ Do you wish to proceed? +
+ )} + {!checkedIn && ( + + )} + +
)} diff --git a/apps/web/src/components/shared/ProfileButton.tsx b/apps/web/src/components/shared/ProfileButton.tsx index 80479377..86410e51 100644 --- a/apps/web/src/components/shared/ProfileButton.tsx +++ b/apps/web/src/components/shared/ProfileButton.tsx @@ -19,6 +19,7 @@ import { DropdownSwitcher } from "@/components/shared/ThemeSwitcher"; import DefaultDropdownTrigger from "../dash/shared/DefaultDropDownTrigger"; import MobileNavBarLinks from "./MobileNavBarLinks"; import { getUser } from "db/functions"; +import { redirect } from "next/navigation"; export default async function ProfileButton() { const clerkUser = await auth(); diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts index b239662a..f34ca64c 100644 --- a/apps/web/src/lib/constants/index.ts +++ b/apps/web/src/lib/constants/index.ts @@ -1,2 +1,3 @@ export const ONE_HOUR_IN_MILLISECONDS = 3600000; +export const FIVE_MINUTES_IN_MILLISECONDS = 300000; export const VERCEL_IP_TIMEZONE_HEADER_KEY = "x-vercel-ip-timezone"; diff --git a/packages/config/hackkit.config.ts b/packages/config/hackkit.config.ts index 58fa86b0..5c5f93ff 100644 --- a/packages/config/hackkit.config.ts +++ b/packages/config/hackkit.config.ts @@ -852,7 +852,7 @@ const c = { Users: "/admin/users", Events: "/admin/events", Points: "/admin/points", - "Check-in": "/admin/check-in", + "Hackathon Check-in": "/admin/check-in", Toggles: "/admin/toggles", }, // TODO: Can remove days? Pretty sure they're dynamic now.