Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Validate Tickets | QRush</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.min.js"></script> | |
| <style> | |
| .scanner-container { | |
| position: relative; | |
| width: 100%; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| .scanner-video { | |
| width: 100%; | |
| border-radius: 0.75rem; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| .scanner-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| border: 3px dashed rgba(255, 255, 255, 0.7); | |
| pointer-events: none; | |
| } | |
| .validation-result { | |
| transition: all 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen bg-gray-100"> | |
| <!-- Navigation --> | |
| <nav class="bg-white shadow-sm"> | |
| <div class="container mx-auto px-6 py-3 flex justify-between items-center"> | |
| <div class="flex items-center space-x-4"> | |
| <i data-feather="qr-code" class="w-6 h-6 text-purple-600"></i> | |
| <span class="text-xl font-bold text-gray-800">QRush</span> | |
| </div> | |
| <a href="index.html" class="text-gray-600 hover:text-purple-600 flex items-center"> | |
| <i data-feather="home" class="w-5 h-5 mr-1"></i> | |
| Back to Home | |
| </a> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <main class="container mx-auto px-6 py-12"> | |
| <div class="text-center mb-12"> | |
| <h1 class="text-3xl font-bold text-gray-800 mb-3">Ticket Validation</h1> | |
| <p class="text-gray-600 max-w-2xl mx-auto">Scan QR codes or manually enter OTP to validate event tickets.</p> | |
| </div> | |
| <div class="flex flex-col lg:flex-row gap-10"> | |
| <!-- Scanner Section --> | |
| <div class="w-full lg:w-1/2"> | |
| <div class="bg-white p-6 rounded-xl shadow-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">QR Scanner</h2> | |
| <button id="toggle-camera" class="text-purple-600 hover:text-purple-800 flex items-center"> | |
| <i data-feather="refresh-cw" class="w-4 h-4 mr-1"></i> | |
| Switch Camera | |
| </button> | |
| </div> | |
| <div class="scanner-container bg-black rounded-lg overflow-hidden mb-4"> | |
| <video id="scanner-video" class="scanner-video" autoplay playsinline></video> | |
| <div class="scanner-overlay"></div> | |
| </div> | |
| <div class="flex items-center justify-center mb-4"> | |
| <div class="h-px bg-gray-200 flex-1"></div> | |
| <span class="mx-4 text-gray-500 text-sm">OR</span> | |
| <div class="h-px bg-gray-200 flex-1"></div> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="manual-code" class="block text-sm font-medium text-gray-700 mb-2">Enter OTP Manually</label> | |
| <div class="flex"> | |
| <input | |
| type="text" | |
| id="manual-code" | |
| placeholder="Enter 6-digit code" | |
| class="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-purple-500" | |
| maxlength="6" | |
| pattern="\d{6}" | |
| title="Please enter a 6-digit code" | |
| > | |
| <button id="validate-btn" class="bg-purple-600 text-white px-4 py-2 rounded-r-lg hover:bg-purple-700 transition duration-300"> | |
| Validate | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="w-full lg:w-1/2"> | |
| <div class="bg-white p-6 rounded-xl shadow-md h-full"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Validation Results</h2> | |
| <div id="validation-result" class="validation-result hidden"> | |
| <div id="valid-ticket" class="hidden"> | |
| <div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4"> | |
| <div class="flex items-center"> | |
| <i data-feather="check-circle" class="w-6 h-6 text-green-500 mr-2"></i> | |
| <span class="text-green-700 font-medium">Valid Ticket</span> | |
| </div> | |
| </div> | |
| <div class="bg-white border border-gray-200 rounded-lg p-4"> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <p class="text-sm text-gray-500">Event ID</p> | |
| <p id="event-id" class="font-medium">05</p> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">OTP Code</p> | |
| <p id="otp-code" class="font-mono font-medium">051234</p> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">Ticket Holder</p> | |
| <p id="ticket-holder" class="font-medium">John Doe</p> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">Status</p> | |
| <p id="ticket-status" class="font-medium">Valid</p> | |
| </div> | |
| </div> | |
| <div class="text-center"> | |
| <button class="bg-green-100 text-green-700 px-4 py-2 rounded-lg text-sm font-medium"> | |
| <i data-feather="check" class="w-4 h-4 inline mr-1"></i> | |
| Entry Approved | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="invalid-ticket" class="hidden"> | |
| <div class="bg-red-50 border border-red-200 rounded-lg p-4"> | |
| <div class="flex items-center"> | |
| <i data-feather="x-circle" class="w-6 h-6 text-red-500 mr-2"></i> | |
| <span class="text-red-700 font-medium">Invalid Ticket</span> | |
| </div> | |
| <p class="mt-2 text-sm text-gray-600" id="invalid-reason">This ticket has already been used or doesn't exist.</p> | |
| </div> | |
| <div class="mt-4 text-center"> | |
| <button class="bg-red-100 text-red-700 px-4 py-2 rounded-lg text-sm font-medium"> | |
| <i data-feather="alert-triangle" class="w-4 h-4 inline mr-1"></i> | |
| Deny Entry | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="no-result" class="text-center py-12"> | |
| <div class="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4"> | |
| <i data-feather="search" class="w-8 h-8 text-gray-400"></i> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-500 mb-2">No ticket scanned yet</h3> | |
| <p class="text-gray-400 text-sm">Scan a QR code or enter OTP manually to validate a ticket.</p> | |
| </div> | |
| <div class="mt-6 border-t border-gray-200 pt-4"> | |
| <h3 class="text-sm font-medium text-gray-500 mb-2">Recent Validations</h3> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-sm"> | |
| <span class="text-gray-700">051234</span> | |
| <span class="text-green-600">Valid</span> | |
| </div> | |
| <div class="flex justify-between text-sm"> | |
| <span class="text-gray-700">056789</span> | |
| <span class="text-red-600">Invalid</span> | |
| </div> | |
| <div class="flex justify-between text-sm"> | |
| <span class="text-gray-700">052345</span> | |
| <span class="text-green-600">Valid</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| feather.replace(); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const video = document.getElementById('scanner-video'); | |
| const validateBtn = document.getElementById('validate-btn'); | |
| const manualCodeInput = document.getElementById('manual-code'); | |
| const validationResult = document.getElementById('validation-result'); | |
| const validTicket = document.getElementById('valid-ticket'); | |
| const invalidTicket = document.getElementById('invalid-ticket'); | |
| const noResult = document.getElementById('no-result'); | |
| const toggleCameraBtn = document.getElementById('toggle-camera'); | |
| let currentFacingMode = 'environment'; | |
| let stream = null; | |
| // Initialize scanner | |
| function initScanner(facingMode) { | |
| stopScanner(); | |
| navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: facingMode, | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 } | |
| }, | |
| audio: false | |
| }).then(function(s) { | |
| stream = s; | |
| video.srcObject = stream; | |
| video.play(); | |
| // Start scanning | |
| scanQRCode(); | |
| }).catch(function(err) { | |
| console.error("Camera error:", err); | |
| }); | |
| } | |
| // Stop scanner | |
| function stopScanner() { | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| } | |
| } | |
| // Scan QR code from video stream | |
| function scanQRCode() { | |
| const canvas = document.createElement('canvas'); | |
| const context = canvas.getContext('2d'); | |
| function tick() { | |
| if (video.readyState === video.HAVE_ENOUGH_DATA) { | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| const imageData = context.getImageData(0, 0, canvas.width, canvas.height); | |
| const code = jsQR(imageData.data, imageData.width, imageData.height); | |
| if (code) { | |
| validateTicket(code.data); | |
| } | |
| } | |
| requestAnimationFrame(tick); | |
| } | |
| tick(); | |
| } | |
| // Validate ticket (mock function) | |
| function validateTicket(code) { | |
| // Show loading state | |
| validationResult.classList.remove('hidden'); | |
| validTicket.classList.add('hidden'); | |
| invalidTicket.classList.add('hidden'); | |
| noResult.classList.add('hidden'); | |
| // Simulate API call | |
| setTimeout(() => { | |
| // Mock validation - odd numbers are valid, even are invalid | |
| const isValid = parseInt(code) % 2 !== 0; | |
| if (isValid) { | |
| document.getElementById('event-id').textContent = code.substring(0, 2); | |
| document.getElementById('otp-code').textContent = code; | |
| document.getElementById('ticket-holder').textContent = "Ticket Holder " + code.substring(2); | |
| document.getElementById('ticket-status').textContent = "Valid"; | |
| validTicket.classList.remove('hidden'); | |
| invalidTicket.classList.add('hidden'); | |
| } else { | |
| document.getElementById('invalid-reason').textContent = | |
| code.length === 6 ? "This ticket has already been used." : "Invalid code format."; | |
| validTicket.classList.add('hidden'); | |
| invalidTicket.classList.remove('hidden'); | |
| } | |
| noResult.classList.add('hidden'); | |
| }, 800); | |
| // Clear manual input | |
| manualCodeInput.value = ''; | |
| } | |
| // Toggle camera | |
| toggleCameraBtn.addEventListener('click', function() { | |
| currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment'; | |
| initScanner(currentFacingMode); | |
| }); | |
| // Manual validation | |
| validateBtn.addEventListener('click', function() { | |
| const code = manualCodeInput.value.trim(); | |
| if (code.length === 6 && /^\d+$/.test(code)) { | |
| validateTicket(code); | |
| } else { | |
| alert("Please enter a valid 6-digit code"); | |
| } | |
| }); | |
| // Initialize with back camera | |
| if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
| initScanner(currentFacingMode); | |
| } else { | |
| alert("Camera access not available in your browser."); | |
| } | |
| // Clean up on page leave | |
| window.addEventListener('beforeunload', function() { | |
| stopScanner(); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |