From d04edbde98097d66aa298816d74d185177de112c Mon Sep 17 00:00:00 2001 From: Lindsay Xie <70488690+Lindsay-X@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:01:47 -0400 Subject: [PATCH 1/3] ADDED A CHECK IN PAGE + added view qr code for user info page :) --- package.json | 1 + src/components/layouts/Dashboard.tsx | 4 +- src/features/applications/index.tsx | 2 +- src/features/qr-checkIn/index.tsx | 186 ++++++++++++++++ src/features/qr-checkIn/userInfoBox.tsx | 235 ++++++++++++++++++++ src/features/user-details/QR.tsx | 34 +++ src/features/user-details/currentStatus.tsx | 1 - src/features/user-details/index.tsx | 23 +- src/routes.ts | 1 + 9 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 src/features/qr-checkIn/index.tsx create mode 100644 src/features/qr-checkIn/userInfoBox.tsx create mode 100644 src/features/user-details/QR.tsx diff --git a/package.json b/package.json index e809a1d..919b5de 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@react-router/node": "^7.0.1", + "@yudiel/react-qr-scanner": "^2.3.1", "axios": "^1.10.0", "isbot": "^5", "react": "^18.3.1", diff --git a/src/components/layouts/Dashboard.tsx b/src/components/layouts/Dashboard.tsx index 0d945fe..47a68e5 100644 --- a/src/components/layouts/Dashboard.tsx +++ b/src/components/layouts/Dashboard.tsx @@ -8,12 +8,14 @@ const organizerPages = [ { label: 'Applications', to: '/apps' }, { label: 'Volunteers', to: '/volunteers' }, { label: 'External Users', to: '/external-users' }, - { label: 'Admin Settings', to: '/admin-settings' }, + { label: 'QR Check In', to: '/qr-checkIn' }, { label: 'Assign NFC', to: '/assign-nfc' }, + { label: 'Admin Settings', to: '/admin-settings' }, ]; const volunteerPages = [ { label: 'Home', to: '/' }, + { label: 'QR Check In', to: '/qr-checkIn' }, { label: 'Assign NFC', to: '/assign-nfc' }, ]; diff --git a/src/features/applications/index.tsx b/src/features/applications/index.tsx index 74b0ee0..23bb2d2 100644 --- a/src/features/applications/index.tsx +++ b/src/features/applications/index.tsx @@ -198,7 +198,7 @@ export default function Applications() { key={user._id} className="bg-gray-100 hover:bg-gray-200 dark:bg-slate-500 dark:hover:bg-slate-700" onClick={() => { - void navigate(`/user/${user._id}`); + window.open(`/user/${user._id}`); }} > {[...columns.entries()].map(([key, value]) => ( diff --git a/src/features/qr-checkIn/index.tsx b/src/features/qr-checkIn/index.tsx new file mode 100644 index 0000000..f512182 --- /dev/null +++ b/src/features/qr-checkIn/index.tsx @@ -0,0 +1,186 @@ +'use client'; + +import { useState } from 'react'; +import { Scanner } from '@yudiel/react-qr-scanner'; +import { getUser, User } from '@/utils/ht6-api'; +import Button from '@/components/button'; +import UserInfoBox from './userInfoBox'; +import AlertPopup from '@/components/alert-popup'; + +interface Point { + x: number; + y: number; +} + +interface QRCodeScanResult { + boundingBox: { + x: number; + y: number; + width: number; + height: number; + top: number; + right: number; + bottom: number; + left: number; + }; + rawValue: string; + format: string; + cornerPoints: Point[]; +} + +export default function QRCheckIn() { + const [cameraOn, setCameraOn] = useState(false); + const [userType, setUserType] = useState<'User' | 'ExternalUser' | null>( + null, + ); + const [scanResult, setScanResult] = useState(null); + const [candidate, setCandidate] = useState(null); + const [showAlert, setShowAlert] = useState(false); + const [alertMessage, setAlertMessage] = useState(''); + const [scannedLog, setScannedLog] = useState([]); + + const handleScan = async (codes: QRCodeScanResult[]) => { + const result = codes[0]?.rawValue; + if (!result) { + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('No QR code result'); + } + if (result == scanResult) { + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('User already scanned and currently being displayed'); + } + + setScanResult(result); + + let parsed; + try { + parsed = JSON.parse(result); + } catch { + console.error('Invalid QR code content:', result); + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage(`Invalid QR code content: ${result}`); + return; + } + + setUserType(parsed.userType); + console.log('Scanned QR:', result); + + setShowAlert(true); + setAlertMessage('Loading...'); + try { + const res = await getUser(1, 2, 'asc', '', '', { _id: parsed.userID }); + setCandidate(res.message[0]); + setShowAlert(false); + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('User loaded'); + setScannedLog((prev) => { + const updated = [res.message[0], ...prev]; + return updated.slice(0, 8); + }); + } catch (error) { + console.error('Search failed:', error); + setCandidate(null); + } + }; + + return ( +
+ {showAlert && } +

QR Check In

+

+ Check in hackers quickly using their QR code +

+ +
+
+ {cameraOn ? + { + console.error('QR scan error', err); + alert('Error accessing camera or scanning QR code.'); + }} + styles={{ + container: { + width: '100%', + height: '300px', + borderRadius: '0.75rem', + overflow: 'hidden', + }, + video: { + objectFit: 'cover', + width: '100%', + height: '100%', + }, + }} + /> + :
+ Camera is off +
+ } + + +
+ +
+

+ Scanned Log +

+
    + {scannedLog.length === 0 ? +
  • No scans yet
  • + : scannedLog.map((user) => ( +
  • + {user.fullName} — {user.email} + +
  • + )) + } +
+
+
+
+ ); +} diff --git a/src/features/qr-checkIn/userInfoBox.tsx b/src/features/qr-checkIn/userInfoBox.tsx new file mode 100644 index 0000000..0a0aaa6 --- /dev/null +++ b/src/features/qr-checkIn/userInfoBox.tsx @@ -0,0 +1,235 @@ +import { User, checkInUser } from '@/utils/ht6-api'; +import React, { useState } from 'react'; +import AlertPopup from '@/components/alert-popup'; +import Button from '@/components/button'; + +export const getImportantInfo = (candidate: User) => [ + { + label: 'ID', + value: candidate._id || 'Not Provided', + svg: ( + + + + ), + copy: true, + }, + { + label: 'Full Name', + value: candidate.fullName || 'Not Provided', + svg: ( + + + + ), + copy: true, + }, + { + label: 'Age', + value: candidate.hackerApplication?.age?.toString() ?? 'Not Provided', + svg: <>, + copy: false, + }, + { + label: 'Email', + value: candidate.email || 'Not Provided', + svg: ( + + + + + ), + copy: true, + }, + { + label: 'Shirt Size', + value: candidate.hackerApplication?.shirtSize ?? 'Not Provided', + svg: <>, + copy: false, + }, + { + label: 'Status', + value: candidate.status.checkedIn ? 'Checked In' : 'Not Checked In', + svg: <>, + copy: false, + important: true, + }, +]; + +export default function UserInfoBox({ + candidate, + userType, +}: { + candidate: User | null | undefined; + userType: 'User' | 'ExternalUser' | null; +}) { + const [showAlert, setShowAlert] = useState(false); + const userInfo = candidate ? getImportantInfo(candidate) : []; + const [alertMessage, setAlertMessage] = useState(''); + + const handleCheckIn = async ( + userID: string, + userType: 'User' | 'ExternalUser', + ) => { + try { + const res = await checkInUser(userID, userType); + if (res.status === 200) { + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('User checked in successfully'); + } else { + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('Check-in failed'); + } + } catch (error) { + setShowAlert(true); + setTimeout(() => { + setShowAlert(false); + }, 3000); + setAlertMessage('Check-in failed'); + console.log(error); + } + }; + + function DisplayInfo({ + label, + svg, + value, + copy, + important, + }: { + label: string; + svg: React.JSX.Element; + value: string; + copy: boolean; + important?: boolean; + }) { + return ( +
+
+ {svg} + {important ? +

+ {label} +

+ :

+ {label} +

+ } +
+
+
+ +

{value}

+
+ {copy && ( + + )} +
+
+ ); + } + + return ( +
+ {showAlert && } +
+

+ Scanned User Info +

+
+ + {candidate ? + userInfo.map((itm) => ( + + )) + :

+ No user scanned +

+ } + + {candidate && ( +
+ +
+ )} +
+ ); +} diff --git a/src/features/user-details/QR.tsx b/src/features/user-details/QR.tsx new file mode 100644 index 0000000..46ce9f0 --- /dev/null +++ b/src/features/user-details/QR.tsx @@ -0,0 +1,34 @@ +export default function QR({ + onClose, + base64Img, +}: { + onClose: () => void; + base64Img: string; +}) { + return ( +
+
+

+ {base64Img != '' ? 'QR Code' : 'QR Failed to Load'} +

+
+ {base64Img != '' && ( + QR Code + )} +
+
+ +
+
+
+ ); +} diff --git a/src/features/user-details/currentStatus.tsx b/src/features/user-details/currentStatus.tsx index 099da0f..1a2d58d 100644 --- a/src/features/user-details/currentStatus.tsx +++ b/src/features/user-details/currentStatus.tsx @@ -15,7 +15,6 @@ export default function CurrentStatus({ candidate }: { candidate: User }) { 'Response: -', 'Checked In', ]; - console.log(candidate); const colourIndex = [1, 0, 0, 0, 0, 0]; if (candidate.status.applied) { diff --git a/src/features/user-details/index.tsx b/src/features/user-details/index.tsx index cfa64b8..362ab7f 100644 --- a/src/features/user-details/index.tsx +++ b/src/features/user-details/index.tsx @@ -12,6 +12,7 @@ import CurrentStatus from './currentStatus'; import Button from '@/components/button'; import SideBarInfo from './sideBarInfo'; import { categoryNames, categoryQuestions } from '@/utils/const'; +import QR from './QR'; export async function clientLoader({ params }: { params: { id: string } }) { const applicantsData = await getUser(1, 2, 'asc', '', '', { _id: params.id }); @@ -34,6 +35,7 @@ export default function UserDetails() { const resumeLink = userData.resumeLink; const id = useParams().id; //const navigation = useNavigate(); + const [showQr, setShowQr] = useState(false); // syncing up candidate info every 10 seconds const fetchCandidate = async () => { @@ -106,16 +108,27 @@ export default function UserDetails() { return (
+ {showQr && ( + { + setShowQr(false); + }} + base64Img={candidate.checkInQR ?? ''} + /> + )}

{candidate.fullName}

-

- Check In | -

- - +
diff --git a/src/routes.ts b/src/routes.ts index 5a1ca01..274852c 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -17,6 +17,7 @@ export default [ route('review', 'features/review/index.tsx'), route('nfc/u/:nfcId', 'features/participant/ParticipantDetail.tsx'), route('assign-nfc', 'features/assign-nfc/index.tsx'), + route('qr-checkIn', 'features/qr-checkIn/index.tsx'), index('features/home/index.tsx'), ]), ...prefix('auth', [ From 4e4f148436003d0e348da6fdbb7d1a028daaf5c4 Mon Sep 17 00:00:00 2001 From: Lindsay Xie <70488690+Lindsay-X@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:07:14 -0400 Subject: [PATCH 2/3] fixed parsed typing --- src/features/qr-checkIn/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/qr-checkIn/index.tsx b/src/features/qr-checkIn/index.tsx index f512182..f097ba0 100644 --- a/src/features/qr-checkIn/index.tsx +++ b/src/features/qr-checkIn/index.tsx @@ -60,7 +60,10 @@ export default function QRCheckIn() { let parsed; try { - parsed = JSON.parse(result); + parsed = JSON.parse(result) as { + userID: string; + userType: 'User' | 'ExternalUser'; + }; } catch { console.error('Invalid QR code content:', result); setShowAlert(true); From ac46c941d3bafb82757f29ff6c85094cf7dc8689 Mon Sep 17 00:00:00 2001 From: Lindsay Xie <70488690+Lindsay-X@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:42:54 -0400 Subject: [PATCH 3/3] tweaked responsiveness --- src/features/qr-checkIn/index.tsx | 58 ++++++++++++++++++------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/features/qr-checkIn/index.tsx b/src/features/qr-checkIn/index.tsx index f097ba0..fed4206 100644 --- a/src/features/qr-checkIn/index.tsx +++ b/src/features/qr-checkIn/index.tsx @@ -58,6 +58,7 @@ export default function QRCheckIn() { setScanResult(result); + // parsing result string let parsed; try { parsed = JSON.parse(result) as { @@ -77,6 +78,7 @@ export default function QRCheckIn() { setUserType(parsed.userType); console.log('Scanned QR:', result); + // getting user info with userId from scanned results setShowAlert(true); setAlertMessage('Loading...'); try { @@ -103,33 +105,34 @@ export default function QRCheckIn() { {showAlert && }

QR Check In

- Check in hackers quickly using their QR code + Check in hackers using their QR code

+ {/* scanner */}
{cameraOn ? - { - console.error('QR scan error', err); - alert('Error accessing camera or scanning QR code.'); - }} - styles={{ - container: { - width: '100%', - height: '300px', - borderRadius: '0.75rem', - overflow: 'hidden', - }, - video: { - objectFit: 'cover', - width: '100%', - height: '100%', - }, - }} - /> - :
+
+ { + console.error('QR scan error', err); + alert('Error accessing camera or scanning QR code.'); + }} + styles={{ + container: { + width: '100%', + height: '100%', + }, + video: { + objectFit: 'cover', + width: '100%', + height: '100%', + }, + }} + /> +
+ :
Camera is off
} @@ -144,7 +147,9 @@ export default function QRCheckIn() { {cameraOn ? 'Turn off Camera' : 'Turn on Camera'}
+ {/* Scanned User Info */} + {/* Log of Past Scanned Users */}

Scanned Log @@ -153,8 +158,13 @@ export default function QRCheckIn() { {scannedLog.length === 0 ?
  • No scans yet
  • : scannedLog.map((user) => ( -
  • - {user.fullName} — {user.email} +
  • +
    + {user.fullName} — {user.email} +