Skip to content

Commit 8da7811

Browse files
EnchLolzNathan Ye
andauthored
feat: implement QR code scanner for Dioxus frontend (#69)
* added QR code scanning to the admin view so now participants can be successfully checked into hackathons. --------- Co-authored-by: Nathan Ye <nathanye@Salmon.local>
1 parent db00865 commit 8da7811

4 files changed

Lines changed: 152 additions & 32 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ sea-orm = { version = "1.1", features = [
3333
], optional = true }
3434
tokio = { version = "1.48.0", features = ["full"], optional = true }
3535
tower = { version = "0.5.2", optional = true }
36-
tower-http = { version = "0.6", features = ["limit"], optional = true }
36+
tower-http = { version = "0.6", features = ["limit", "set-header"], optional = true }
3737
tower-sessions = { version = "0.14.0", optional = true }
3838
tower-sessions-redis-store = { version = "0.16.0", optional = true }
3939
tracing = { version = "0.1", optional = true }

index.html

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
<!doctype html>
22
<html>
3-
<head>
4-
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1" />
6-
<meta charset="UTF-8" />
7-
<link rel="manifest" href="/api/manifest.json" />
8-
<meta name="theme-color" content="#ffffff" />
9-
<meta name="apple-mobile-web-app-capable" content="yes" />
10-
<meta
11-
name="apple-mobile-web-app-status-bar-style"
12-
content="black-translucent"
13-
/>
14-
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
15-
16-
<title>Terrier</title>
17-
</head>
18-
<body>
19-
<div id="main"></div>
20-
</body>
3+
4+
<head>
5+
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta charset="UTF-8" />
8+
<link rel="manifest" href="/api/manifest.json" />
9+
<meta name="theme-color" content="#ffffff" />
10+
<meta name="apple-mobile-web-app-capable" content="yes" />
11+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
12+
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
13+
14+
<title>Terrier</title>
15+
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
16+
</head>
17+
18+
<body>
19+
<div id="main"></div>
20+
</body>
21+
2122
</html>

src/ui/features/dashboard/qr_tile.rs

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,125 @@ fn QRDisplay(qr_svg: String, user_id: i32) -> Element {
8585

8686
/// Fullscreen QR code modal
8787
#[component]
88-
pub fn QRModal(qr_svg: String, user_id: i32, on_close: EventHandler<()>) -> Element {
88+
pub fn QRModal(
89+
qr_svg: String,
90+
user_id: i32,
91+
on_close: EventHandler<()>,
92+
on_scan: Option<EventHandler<String>>,
93+
) -> Element {
94+
let show_scanner = on_scan.is_some();
95+
8996
rsx! {
9097
// Backdrop - covers entire screen with semi-transparent grey
9198
div {
9299
class: "fixed inset-0 flex items-center justify-center z-50",
93100
style: "background-color: rgba(0, 0, 0, 0.7);",
94-
onclick: move |_| on_close.call(()),
101+
onclick: move |_| {
102+
// Attempt to stop scanner JS
103+
if show_scanner {
104+
let mut eval = document::eval("if (window.stopQrScanner) window.stopQrScanner();");
105+
// We don't need to wait for it
106+
}
107+
on_close.call(());
108+
},
95109

96-
// Modal content - centered QR code
110+
// Modal content - centered QR code or scanner
97111
div {
98112
class: "relative flex flex-col items-center justify-center",
99113
onclick: move |e| e.stop_propagation(),
100114

101-
// Large QR code display
102-
div { class: "w-[95vmin] h-[95vmin] max-w-[500px] max-h-[500px] flex flex-col items-center justify-center gap-4",
103-
div {
104-
class: "w-full h-full bg-background-neutral-primary rounded-2xl",
105-
dangerous_inner_html: "{qr_svg}",
115+
// Display Area
116+
div { class: "w-[95vmin] h-[95vmin] max-w-[500px] max-h-[500px] flex flex-col items-center justify-center gap-4 bg-background-neutral-primary rounded-2xl p-4",
117+
if let Some(handler) = on_scan {
118+
Scanner { on_scan: handler }
119+
} else {
120+
div {
121+
class: "w-full h-full bg-background-neutral-primary rounded-2xl",
122+
dangerous_inner_html: "{qr_svg}",
123+
}
124+
div { class: "text-white font-semibold text-lg", "User ID: {user_id}" }
125+
}
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
#[component]
133+
fn Scanner(on_scan: EventHandler<String>) -> Element {
134+
// Use eval to initialize the scanner
135+
// We use use_future to run this once on mount
136+
let mut eval = document::eval(
137+
r#"
138+
const scanHandler = await dioxus.recv();
139+
140+
function onScanSuccess(decodedText, decodedResult) {
141+
142+
// Check if URL matches our expected format
143+
if (decodedText.includes("/scan/")) {
144+
const parts = decodedText.split("/scan/");
145+
if (parts.length === 2) {
146+
const userId = parts[1];
147+
148+
// Stop scanning immediately
149+
if (window.html5QrcodeScanner) {
150+
window.html5QrcodeScanner.clear().then(() => {
151+
// Send back to Rust after clearing
152+
dioxus.send(userId);
153+
}).catch(err => {
154+
console.error("Failed to clear scanner", err);
155+
dioxus.send(userId);
156+
});
157+
} else {
158+
dioxus.send(userId);
106159
}
107-
div { class: "text-white font-semibold text-lg", "User ID: {user_id}" }
108160
}
109161
}
110162
}
163+
164+
function onScanFailure(error) {
165+
// handle scan failure
166+
}
167+
168+
window.stopQrScanner = function() {
169+
if (window.html5QrcodeScanner) {
170+
window.html5QrcodeScanner.clear();
171+
}
172+
};
173+
174+
setTimeout(() => {
175+
if (document.getElementById('reader')) {
176+
window.html5QrcodeScanner = new Html5QrcodeScanner(
177+
"reader",
178+
{ fps: 10, qrbox: {width: 250, height: 250} },
179+
false);
180+
window.html5QrcodeScanner.render(onScanSuccess, onScanFailure);
181+
}
182+
}, 100);
183+
"#,
184+
);
185+
186+
// Handle scan result
187+
use_future(move || {
188+
let mut eval = eval.clone();
189+
let scan_handler = on_scan.clone();
190+
async move {
191+
eval.send(true).unwrap(); // Start the script
192+
if let Ok(scanned_user_id) = eval.recv::<String>().await {
193+
scan_handler.call(scanned_user_id);
194+
}
195+
}
196+
});
197+
198+
rsx! {
199+
div { class: "w-full text-center text-lg font-semibold mb-2", "Scan Participant QR" }
200+
div {
201+
id: "reader",
202+
class: "w-full bg-black rounded-xl overflow-hidden shadow-lg border border-gray-800",
203+
style: "min-height: 300px;"
204+
}
205+
div { class: "text-sm text-foreground-neutral-secondary text-center mt-2",
206+
"Point camera at a check-in QR Code"
207+
}
111208
}
112209
}

src/ui/pages/hackathon/checkin.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,8 @@ fn EventCategorySection(
597597
#[component]
598598
fn EventDetailPanel(slug: String, event: ScheduleEvent, on_refresh: EventHandler<()>) -> Element {
599599
let slug_for_attendees = slug.clone();
600+
let slug_for_scanner = slug.clone();
601+
let nav = use_navigator();
600602
let event_id = event.id;
601603

602604
// Participant ID input
@@ -606,6 +608,7 @@ fn EventDetailPanel(slug: String, event: ScheduleEvent, on_refresh: EventHandler
606608
// Confirmation modal state
607609
let mut pending_participant: Signal<Option<ParticipantInfo>> = use_signal(|| None);
608610
let mut is_confirming = use_signal(|| false);
611+
let mut show_scanner_modal = use_signal(|| false);
609612
let mut error_message: Signal<Option<String>> = use_signal(|| None);
610613
let mut skip_confirmation = use_signal(|| {
611614
// Check localStorage for skip preference
@@ -668,8 +671,10 @@ fn EventDetailPanel(slug: String, event: ScheduleEvent, on_refresh: EventHandler
668671
"Check-in"
669672
}
670673

671-
// Scan QR placeholder
672-
button { class: "w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-stroke-neutral-1 hover:bg-background-neutral-secondary transition-colors mb-3",
674+
// Scan QR Code
675+
button {
676+
class: "w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-stroke-neutral-1 hover:bg-background-neutral-secondary transition-colors mb-3",
677+
onclick: move |_| show_scanner_modal.set(true),
673678
Icon { width: 20, height: 20, icon: LdQrCode }
674679
span { class: "text-foreground-neutral-primary", "Scan QR code" }
675680
}
@@ -811,8 +816,25 @@ fn EventDetailPanel(slug: String, event: ScheduleEvent, on_refresh: EventHandler
811816
}
812817
}
813818

814-
// Confirmation modal
815-
if let Some(participant) = pending_participant() {
819+
// Scanner Modal
820+
if show_scanner_modal() {
821+
QRModal {
822+
// Props for QRModal (dummy values for scanner mode)
823+
qr_svg: String::new(),
824+
user_id: 0,
825+
on_close: move |_| show_scanner_modal.set(false),
826+
on_scan: move |scanned_id: String| {
827+
let slug = slug_for_scanner.clone();
828+
show_scanner_modal.set(false);
829+
if let Ok(user_id) = scanned_id.parse::<i32>() {
830+
nav.push(Route::HackathonScan { slug: slug.clone(), user_id });
831+
}
832+
}
833+
}
834+
}
835+
836+
// Confirmation modal
837+
if let Some(participant) = pending_participant() {
816838
div {
817839
class: "fixed inset-0 flex items-center justify-center z-50",
818840
style: "background-color: rgba(0, 0, 0, 0.5);",

0 commit comments

Comments
 (0)