dec6446d7d
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
229 lines
5.4 KiB
HTML
229 lines
5.4 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ 'scan_qr'|t }} - NetEvents{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="scan-page">
|
||
<div class="scan-header">
|
||
<h1>{{ 'scan_attendee'|t }}</h1>
|
||
<p>{{ 'scan_qr_description'|t }}</p>
|
||
<a href="{{ url_for('attendee_dashboard') }}" class="btn btn-outline">{{ 'back_to_dashboard'|t }}</a>
|
||
</div>
|
||
|
||
<div class="scanner-container">
|
||
<div id="qr-reader" class="qr-reader"></div>
|
||
<div id="scan-result" class="scan-result hidden">
|
||
<div class="result-icon" id="result-icon"></div>
|
||
<h2 id="result-title"></h2>
|
||
<p id="result-message"></p>
|
||
<button id="scan-again" class="btn btn-primary">{{ 'scan_again'|t }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="scan-info">
|
||
<p>{{ 'scan_info_text'|t }}</p>
|
||
<p><strong>{{ 'scan_info_warning'|t }}</strong></p>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.scan-page {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.scan-header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.scan-header h1 {
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.scan-header p {
|
||
color: #666;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.scanner-container {
|
||
background: #000;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
min-height: 300px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.qr-reader {
|
||
width: 100%;
|
||
}
|
||
|
||
.qr-reader video {
|
||
width: 100% !important;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.scan-result {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
background: #fff;
|
||
width: 100%;
|
||
}
|
||
|
||
.scan-result.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.result-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.result-icon.success::before {
|
||
content: '✓';
|
||
color: #22c55e;
|
||
}
|
||
|
||
.result-icon.pending::before {
|
||
content: '⏳';
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.result-icon.error::before {
|
||
content: '✗';
|
||
color: #ef4444;
|
||
}
|
||
|
||
.result-icon.info::before {
|
||
content: 'ℹ';
|
||
color: #3b82f6;
|
||
}
|
||
|
||
#result-title {
|
||
margin: 0 0 10px 0;
|
||
}
|
||
|
||
#result-message {
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.scan-info {
|
||
background: #f8fafc;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-top: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.scan-info p {
|
||
margin: 10px 0;
|
||
color: #64748b;
|
||
}
|
||
|
||
.scan-info strong {
|
||
color: #1e293b;
|
||
}
|
||
</style>
|
||
|
||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||
<script>
|
||
const myId = {{ session.user_id }};
|
||
const myEventId = {{ session.event_id }};
|
||
let html5QrCode;
|
||
let scanning = true;
|
||
|
||
async function sendConnectionRequest(scannedId) {
|
||
try {
|
||
const response = await fetch('/attendee/scan-request', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ scanned_id: scannedId })
|
||
});
|
||
|
||
const data = await response.json();
|
||
const resultDiv = document.getElementById('scan-result');
|
||
const icon = document.getElementById('result-icon');
|
||
const title = document.getElementById('result-title');
|
||
const message = document.getElementById('result-message');
|
||
|
||
icon.className = 'result-icon';
|
||
|
||
if (data.success) {
|
||
document.getElementById('qr-reader').style.display = 'none';
|
||
resultDiv.classList.remove('hidden');
|
||
icon.classList.add('pending');
|
||
title.textContent = '{{ "request_sent"|t }}';
|
||
message.textContent = '{{ "request_sent_message"|t }}';
|
||
} else {
|
||
document.getElementById('qr-reader').style.display = 'none';
|
||
resultDiv.classList.remove('hidden');
|
||
icon.classList.add('error');
|
||
title.textContent = '{{ "error"|t }}';
|
||
message.textContent = data.error || '{{ "failed_send_request"|t }}';
|
||
}
|
||
|
||
scanning = false;
|
||
} catch (error) {
|
||
console.error('Connection error:', error);
|
||
alert('Error sending connection request');
|
||
}
|
||
}
|
||
|
||
function onScanSuccess(decodedText) {
|
||
if (!scanning) return;
|
||
|
||
// Expected format: "NETEVENT:{event_id}:{attendee_id}"
|
||
const parts = decodedText.split(':');
|
||
if (parts.length === 3 && parts[0] === 'NETEVENT') {
|
||
const eventId = parseInt(parts[1]);
|
||
const attendeeId = parseInt(parts[2]);
|
||
|
||
if (eventId === myEventId) {
|
||
if (attendeeId === myId) {
|
||
alert('{{ "cannot_scan_own_qr"|t }}');
|
||
return;
|
||
}
|
||
sendConnectionRequest(attendeeId);
|
||
} else {
|
||
alert('{{ "qr_different_event"|t }}');
|
||
}
|
||
} else {
|
||
console.log('Unknown QR format:', decodedText);
|
||
alert('{{ "unrecognized_qr"|t }}');
|
||
}
|
||
}
|
||
|
||
function startScanner() {
|
||
html5QrCode = new Html5Qrcode("qr-reader");
|
||
|
||
html5QrCode.start(
|
||
{ facingMode: "environment" },
|
||
{
|
||
fps: 10,
|
||
qrbox: { width: 250, height: 250 }
|
||
},
|
||
onScanSuccess,
|
||
(errorMessage) => {
|
||
// Ignore scan errors
|
||
}
|
||
).catch(err => {
|
||
console.error('Camera error:', err);
|
||
alert('{{ "camera_permission_error"|t }}');
|
||
});
|
||
}
|
||
|
||
function resetScanner() {
|
||
document.getElementById('qr-reader').style.display = 'block';
|
||
document.getElementById('scan-result').classList.add('hidden');
|
||
scanning = true;
|
||
}
|
||
|
||
document.getElementById('scan-again').addEventListener('click', resetScanner);
|
||
|
||
window.addEventListener('DOMContentLoaded', startScanner);
|
||
</script>
|
||
{% endblock %} |