qrush-ticketing-magic / validate.html
wixdivin's picture
MVP Description: QR-Based Event Ticketing and Validation System
5614052 verified
<!DOCTYPE html>
<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>