Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Hackathon Check-In Scanner #130

Merged
merged 6 commits into from
Oct 24, 2024
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
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
Loading