Skip to content

Commit

Permalink
Fix Hackathon Check-In Scanner (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
christianhelp authored Oct 24, 2024
1 parent 2123259 commit 638fd57
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 105 deletions.
22 changes: 17 additions & 5 deletions apps/web/src/actions/admin/scanner-admin-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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));
});
4 changes: 3 additions & 1 deletion apps/web/src/app/admin/check-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export default async function Page({
);

const scanUser = await getUser(searchParams.user);
if (!scanUser)
console.log(scanUser);
if (!scanUser) {
return (
<div>
<CheckinScanner
Expand All @@ -30,6 +31,7 @@ export default async function Page({
/>
</div>
);
}

return (
<div>
Expand Down
203 changes: 105 additions & 98 deletions apps/web/src/components/admin/scanner/CheckinScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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 (
<>
<div className="flex h-dvh flex-col items-center justify-center pt-32">
Expand Down Expand Up @@ -108,11 +161,6 @@ export default function CheckinScanner({
}}
/>
</div>
{/* <div className="mx-auto flex w-screen max-w-[500px] justify-center gap-x-2 overflow-hidden">
<Link href={"/admin/events"}>
<Button>Return To Events</Button>
</Link>
</div> */}
</div>
</div>
<Drawer
Expand All @@ -124,7 +172,6 @@ export default function CheckinScanner({
<>
<DrawerHeader>
<DrawerTitle>Loading Scan...</DrawerTitle>
{/* <DrawerDescription></DrawerDescription> */}
</DrawerHeader>
<DrawerFooter>
<Button
Expand All @@ -138,80 +185,40 @@ export default function CheckinScanner({
) : (
<>
<DrawerHeader>
{checkedIn ? (
<DrawerTitle className="mx-auto">
User already checked in!
</DrawerTitle>
) : (
<>
{!proceed ? (
<>
<DrawerTitle className="text-red-500">
Warning!
</DrawerTitle>
<DrawerDescription>
{scanUser?.firstName}{" "}
{scanUser?.lastName} Is not
RSVP'd
</DrawerDescription>
<DrawerFooter>
Do you wish to proceed?
<Button
onClick={() => {
setProceed(true);
}}
variant="outline"
>
Proceed
</Button>
<Button
onClick={() =>
router.replace(path)
}
variant="outline"
>
Cancel
</Button>
</DrawerFooter>
</>
) : (
<>
<DrawerTitle>
New Scan
</DrawerTitle>
<DrawerDescription>
New scan for{" "}
{scanUser?.firstName}{" "}
{scanUser?.lastName}
</DrawerDescription>
</>
)}
</>
)}
<DrawerTitle
className={clsx("mx-auto", {
"text-red-500": !hasRSVP || checkedIn,
})}
>
{drawerTitle}
</DrawerTitle>
</DrawerHeader>
{proceed ? (
<>
<DrawerFooter>
{!checkedIn && (
<Button
onClick={() =>
handleScanCreate()
}
>
{"Scan User In"}
</Button>
)}
<Button
onClick={() => router.replace(path)}
variant="outline"
>
Cancel
</Button>
</DrawerFooter>
</>
) : (
<></>
)}
<DrawerDescription className="mx-auto">
{drawerDescription}
</DrawerDescription>
<DrawerFooter>
{!hasRSVP && !checkedIn && (
<div className="mx-auto">
Do you wish to proceed?
</div>
)}
{!checkedIn && (
<Button
onClick={() => {
handleScanCreate();
}}
variant="outline"
>
{drawerFooterButtonText}
</Button>
)}
<Button
onClick={() => router.replace(path)}
variant="outline"
>
Cancel
</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/shared/ProfileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/constants/index.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion packages/config/hackkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 638fd57

Please sign in to comment.