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..fed4206 --- /dev/null +++ b/src/features/qr-checkIn/index.tsx @@ -0,0 +1,199 @@ +'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); + + // parsing result string + let parsed; + try { + parsed = JSON.parse(result) as { + userID: string; + userType: 'User' | 'ExternalUser'; + }; + } 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); + + // getting user info with userId from scanned results + 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 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: '100%', + }, + video: { + objectFit: 'cover', + width: '100%', + height: '100%', + }, + }} + /> +
+ :
+ Camera is off +
+ } + + +
+ {/* Scanned User Info */} + + {/* Log of Past Scanned Users */} +
+

+ 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', [