Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/components/layouts/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
];

Expand Down
2 changes: 1 addition & 1 deletion src/features/applications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => (
Expand Down
199 changes: 199 additions & 0 deletions src/features/qr-checkIn/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);
const [candidate, setCandidate] = useState<User | null>(null);
const [showAlert, setShowAlert] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
const [scannedLog, setScannedLog] = useState<User[]>([]);

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 (
<div className="p-4 m-7 text-left">
{showAlert && <AlertPopup message={alertMessage} />}
<h1 className="text-5xl font-bold text-primary">QR Check In</h1>
<p className="text-xl font-bold text-slate-500">
Check in hackers using their QR code
</p>

<div className="grid md:grid-cols-3 mt-8 gap-6">
{/* scanner */}
<div className="relative bg-primary-extralight border border-primary dark:bg-slate-700 rounded-2xl shadow-xl p-6 w-full flex items-center justify-center">
{cameraOn ?
<div className="w-full h-[250px] md:h-[300px] rounded-xl overflow-hidden">
<Scanner
onScan={handleScan}
onError={(err) => {
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%',
},
}}
/>
</div>
: <div className="w-full h-[250px] md:h-[300px] flex items-center justify-center text-slate-500">
Camera is off
</div>
}

<Button
onClick={() => {
setCameraOn(!cameraOn);
}}
buttonType="secondary"
className="absolute top-4 right-4 px-4 py-1 text-sm rounded-full shadow-md"
>
{cameraOn ? 'Turn off Camera' : 'Turn on Camera'}
</Button>
</div>
{/* Scanned User Info */}
<UserInfoBox candidate={candidate} userType={userType}></UserInfoBox>
{/* Log of Past Scanned Users */}
<div className="p-5 bg-primary-extralight border border-primary shadow-xl dark:bg-slate-700 rounded-2xl flex flex-col gap-4">
<h2 className="text-gray-700 dark:text-slate-200 font-medium text-lg">
Scanned Log
</h2>
<ul className="text-sm text-gray-700 dark:text-slate-200 overflow-auto">
{scannedLog.length === 0 ?
<li className="italic text-slate-400">No scans yet</li>
: scannedLog.map((user) => (
<li
key={user._id}
className="py-1 flex flex-row justify-between"
>
<div>
<strong>{user.fullName}</strong> — {user.email}
</div>
<button
className="bg-transparent hover:bg-transparent"
onClick={() => {
navigator.clipboard.writeText(user.fullName).catch(() => {
console.error('Failed to copy text');
});
setShowAlert(true);
setTimeout(() => {
setShowAlert(false);
}, 1500);
setAlertMessage(`"${user.fullName}" copied to clipboard`);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="size-5 text-primary-medlight hover:text-primary-dark"
>
<path d="M7 3.5A1.5 1.5 0 0 1 8.5 2h3.879a1.5 1.5 0 0 1 1.06.44l3.122 3.12A1.5 1.5 0 0 1 17 6.622V12.5a1.5 1.5 0 0 1-1.5 1.5h-1v-3.379a3 3 0 0 0-.879-2.121L10.5 5.379A3 3 0 0 0 8.379 4.5H7v-1Z" />
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v9A1.5 1.5 0 0 0 4.5 18h7a1.5 1.5 0 0 0 1.5-1.5v-5.879a1.5 1.5 0 0 0-.44-1.06L9.44 6.439A1.5 1.5 0 0 0 8.378 6H4.5Z" />
</svg>
</button>
</li>
))
}
</ul>
</div>
</div>
</div>
);
}
Loading