Spaces:
Running
MVP Description: QR-Based Event Ticketing and Validation System
Browse filesThis MVP (Minimum Viable Product) describes a complete QR Code Ticketing and Validation System, where tickets are generated as QR codes embedding secure numeric identifiers. The system’s goal is to simplify event access verification by combining a numeric encoding scheme, QR generation, manual OTP fallback, and database validation. It ensures both offline usability and fast validation, providing a seamless experience for organizers and attendees.
1. MVP Core Concept
The app’s foundation is built on a numeric QR encoding process. Instead of embedding user data directly (which can be insecure and verbose), each QR code contains a simple 6-digit numeric code, uniquely identifying a purchased ticket.
The structure of the numeric code is defined as follows:[EE][UUUU]
EE (2 digits) – represents the event ID.
UUUU (4 digits) – a random numeric code assigned to a user’s ticket.
For example, if the event ID is 05 (Music Festival) and the user’s random code is 1234, the resulting QR and OTP would be 051234. This number acts as both the encoded QR payload and a manual OTP fallback, ensuring validation works even if QR scanning fails.
2. User Flow
The MVP flow is simple, efficient, and covers all user scenarios:
Ticket Purchase
The user selects an event and enters their name.
The system generates a random 4-digit code.
Combines event ID + user code → final OTP like 051234.
QR Code Generation
The backend encodes 051234 using a QR generator (e.g., QRCode.toDataURL).
It stores the QR data (base64), event ID, buyer name, OTP, and usage status in the database.
Ticket Validation
At the event, the admin scans the QR using a web or mobile scanner.
The scanner decodes the number (e.g., “051234”).
The backend looks up the database entry for that OTP.
If found and unused → mark as used → return Valid ✅.
If not found or already used → return Invalid ❌.
Manual Validation
If a QR scan fails, the admin can type the same 6-digit OTP manually.
The system performs the same lookup and validation logic.
3. MVP Feature Breakdown
🎫 Ticket Generation Module
Input: eventId, buyerName
Process:
Generate random user code (4 digits)
Combine with eventId
Generate QR code
Store record in DB
Return QR image and OTP code
Output: { qrCode: "base64", otpCode: "051234" }
🔍 Ticket Validation Module
Input: code (from QR scan or manual)
Process:
Find ticket by otpCode
If exists and not used → mark used
If invalid or used → return false
Output: { valid: true, eventId, buyerName }
📷 QR Scanner Module
Uses camera to detect and decode numeric QR data.
If no camera access, fallback to manual OTP input.
Displays validation feedback visually.
💻 Admin Validation Page
Real-time QR scanning using React/JS QR library.
Manual code entry field.
Displays “✅ Valid” or “❌ Invalid”.
Logs validation events.
4. Database Structure
A minimal Tickets table captures all essential fields.
This allows tracking of each generated QR code, its corresponding event, and usage state.
Column Type Description
id UUID Primary key
eventId INT 2-digit event identifier
userCode INT 4-digit random ticket code
otpCode STRING Combination of eventId + userCode
qrCode STRING Base64 image data
buyerName STRING Purchaser name
used BOOLEAN Marks if the ticket has been validated
createdAt DATETIME Timestamp of creation{
"id": "uuid-1234",
"eventId": 5,
"userCode": 1234,
"otpCode": "051234",
"qrCode": "data:image/png;base64,...",
"buyerName": "John Doe",
"used": false,
"createdAt": "2025-10-17T08:00:00Z"
}
{
"backend": {
"api": {
"/generateTicket": {
"method": "POST",
"input": {
"eventId": "number",
"buyerName": "string"
},
"process": [
"Generate 4 random digits",
"Combine with eventId to form otp",
"Generate QR image for otp",
"Save record to database",
"Return qrCode and otp"
],
"response": {
"qrCode": "base64 image string",
"otpCode": "string"
}
},
"/validateTicket": {
"method": "POST",
"input": {
"code": "string"
},
"process": [
"Check if code exists and not used",
"If valid: mark as used, return success",
"Else return invalid"
],
"response": {
"valid": "boolean",
"buyerName": "string",
"eventId": "number"
}
}
}
}
}
{
"frontend": {
"pages": {
"HomePage": {
"features": ["Show available events", "Buy Ticket button"]
},
"BuyTicketPage": {
"features": [
"Form to select event and enter name",
"Call /generateTicket API",
"Display QR image + numeric OTP",
"Option to download or share QR"
]
},
"ValidationPage": {
"features": [
"Live QR Scanner (camera)",
"Manual OTP entry field",
"Real-time validation result display"
]
},
"AdminPage": {
"features": [
"View all generated tickets",
"See validation logs",
"Reset or manage event codes"
]
}
},
"components": {
"QRCodeDisplay": "Shows generated QR and OTP",
"QRScanner": "Handles scanning and decoding",
"ValidationFeedback": "Displays valid/invalid status"
}
}
}
{
"frontend": {
"pages": {
"HomePage": {
"features": ["Show available events", "Buy Ticket button"]
},
"BuyTicketPage": {
"features": [
"Form to select event and enter name",
"Call /generateTicket API",
"Display QR image + numeric OTP",
"Option to download or share QR"
]
},
"ValidationPage": {
"features": [
"Live QR Scanner (camera)",
"Manual OTP entry field",
"Real-time validation result display"
]
},
"AdminPage": {
"features": [
"View all generated tickets",
"See validation logs",
"Reset or manage event codes"
]
}
},
"components": {
"QRCodeDisplay": "Shows generated QR and OTP",
"QRScanner": "Handles scanning and decoding",
"ValidationFeedback": "Displays valid/invalid status"
}
}
}
7. Security and Integrity
The app’s security model emphasizes simplicity and one-time-use validation.
Mechanism Purpose
Short Numeric Codes Prevents leakage of user data inside QR
Server Validation Ensures no fake or tampered QR passes
One-Time Use Flag Prevents multiple entries with same ticket
Manual OTP Fallback Keeps system functional in camera-restricted environments
Optional Expiry Event codes can expire automatically after event date
- README.md +8 -5
- generate.html +274 -0
- index.html +257 -18
- validate.html +326 -0
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: QRush Ticketing Magic ✨
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: gray
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://deepsite.hf.co).
|
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Generate Tickets | QRush</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.min.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
.ticket-card {
|
| 12 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 13 |
+
transition: all 0.3s ease;
|
| 14 |
+
}
|
| 15 |
+
.ticket-card:hover {
|
| 16 |
+
transform: translateY(-5px);
|
| 17 |
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
| 18 |
+
}
|
| 19 |
+
.qr-placeholder {
|
| 20 |
+
background-color: #f0f0f0;
|
| 21 |
+
background-image:
|
| 22 |
+
linear-gradient(45deg, #e0e0e0 25%, transparent 25%, transparent 75%, #e0e0e0 75%, #e0e0e0),
|
| 23 |
+
linear-gradient(45deg, #e0e0e0 25%, transparent 25%, transparent 75%, #e0e0e0 75%, #e0e0e0);
|
| 24 |
+
background-size: 20px 20px;
|
| 25 |
+
background-position: 0 0, 10px 10px;
|
| 26 |
+
}
|
| 27 |
+
</style>
|
| 28 |
+
</head>
|
| 29 |
+
<body class="min-h-screen bg-gray-50">
|
| 30 |
+
<!-- Navigation -->
|
| 31 |
+
<nav class="bg-white shadow-sm">
|
| 32 |
+
<div class="container mx-auto px-6 py-3 flex justify-between items-center">
|
| 33 |
+
<div class="flex items-center space-x-4">
|
| 34 |
+
<i data-feather="qr-code" class="w-6 h-6 text-purple-600"></i>
|
| 35 |
+
<span class="text-xl font-bold text-gray-800">QRush</span>
|
| 36 |
+
</div>
|
| 37 |
+
<a href="index.html" class="text-gray-600 hover:text-purple-600 flex items-center">
|
| 38 |
+
<i data-feather="home" class="w-5 h-5 mr-1"></i>
|
| 39 |
+
Back to Home
|
| 40 |
+
</a>
|
| 41 |
+
</div>
|
| 42 |
+
</nav>
|
| 43 |
+
|
| 44 |
+
<!-- Main Content -->
|
| 45 |
+
<main class="container mx-auto px-6 py-12">
|
| 46 |
+
<div class="text-center mb-12">
|
| 47 |
+
<h1 class="text-3xl font-bold text-gray-800 mb-3">Generate Tickets</h1>
|
| 48 |
+
<p class="text-gray-600 max-w-2xl mx-auto">Create QR code tickets with OTP fallback for your event.</p>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="flex flex-col lg:flex-row gap-10">
|
| 52 |
+
<!-- Form Section -->
|
| 53 |
+
<div class="w-full lg:w-1/2">
|
| 54 |
+
<div class="bg-white p-6 rounded-xl shadow-md">
|
| 55 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-6">Ticket Details</h2>
|
| 56 |
+
|
| 57 |
+
<form id="ticket-form">
|
| 58 |
+
<div class="mb-6">
|
| 59 |
+
<label for="event-select" class="block text-sm font-medium text-gray-700 mb-2">Select Event</label>
|
| 60 |
+
<select id="event-select" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
|
| 61 |
+
<option value="">-- Select an event --</option>
|
| 62 |
+
<option value="05">Music Festival (ID: 05)</option>
|
| 63 |
+
<option value="12">Tech Conference (ID: 12)</option>
|
| 64 |
+
<option value="18">Art Exhibition (ID: 18)</option>
|
| 65 |
+
<option value="23">Sports Game (ID: 23)</option>
|
| 66 |
+
</select>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="mb-6">
|
| 70 |
+
<label for="ticket-type" class="block text-sm font-medium text-gray-700 mb-2">Ticket Type</label>
|
| 71 |
+
<select id="ticket-type" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
|
| 72 |
+
<option value="general">General Admission</option>
|
| 73 |
+
<option value="vip">VIP</option>
|
| 74 |
+
<option value="student">Student</option>
|
| 75 |
+
<option value="earlybird">Early Bird</option>
|
| 76 |
+
</select>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="mb-6">
|
| 80 |
+
<label for="attendee-name" class="block text-sm font-medium text-gray-700 mb-2">Attendee Name</label>
|
| 81 |
+
<input
|
| 82 |
+
type="text"
|
| 83 |
+
id="attendee-name"
|
| 84 |
+
placeholder="Enter attendee's full name"
|
| 85 |
+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
| 86 |
+
required
|
| 87 |
+
>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<div class="mb-6">
|
| 91 |
+
<label for="ticket-count" class="block text-sm font-medium text-gray-700 mb-2">Number of Tickets</label>
|
| 92 |
+
<input
|
| 93 |
+
type="number"
|
| 94 |
+
id="ticket-count"
|
| 95 |
+
min="1"
|
| 96 |
+
max="10"
|
| 97 |
+
value="1"
|
| 98 |
+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
| 99 |
+
>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div class="flex justify-end">
|
| 103 |
+
<button
|
| 104 |
+
type="submit"
|
| 105 |
+
class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 transition duration-300 flex items-center"
|
| 106 |
+
>
|
| 107 |
+
<i data-feather="plus" class="w-5 h-5 mr-2"></i>
|
| 108 |
+
Generate Tickets
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
</form>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<!-- Preview Section -->
|
| 116 |
+
<div class="w-full lg:w-1/2">
|
| 117 |
+
<div class="bg-white p-6 rounded-xl shadow-md">
|
| 118 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-6">Ticket Preview</h2>
|
| 119 |
+
|
| 120 |
+
<div id="ticket-preview" class="ticket-card p-6 rounded-lg border border-gray-200 mb-6">
|
| 121 |
+
<div id="empty-state" class="text-center py-12">
|
| 122 |
+
<div class="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
| 123 |
+
<i data-feather="ticket" class="w-8 h-8 text-gray-400"></i>
|
| 124 |
+
</div>
|
| 125 |
+
<h3 class="text-lg font-medium text-gray-500 mb-2">No ticket generated yet</h3>
|
| 126 |
+
<p class="text-gray-400 text-sm">Fill out the form to generate a new ticket.</p>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<div id="ticket-content" class="hidden">
|
| 130 |
+
<div class="flex justify-between items-start mb-4">
|
| 131 |
+
<div>
|
| 132 |
+
<h3 id="preview-event" class="font-bold text-lg text-gray-800">Music Festival</h3>
|
| 133 |
+
<p id="preview-date" class="text-sm text-gray-600">October 25, 2023</p>
|
| 134 |
+
</div>
|
| 135 |
+
<div id="preview-type" class="bg-purple-100 px-3 py-1 rounded-full">
|
| 136 |
+
<span class="text-purple-700 text-sm font-medium">General</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div class="flex flex-col items-center mb-4">
|
| 141 |
+
<div id="qr-code" class="w-40 h-40 mb-3 bg-white p-2 rounded qr-placeholder flex items-center justify-center">
|
| 142 |
+
<i data-feather="image" class="w-10 h-10 text-gray-300"></i>
|
| 143 |
+
</div>
|
| 144 |
+
<p class="text-sm text-gray-600">OTP: <span id="preview-otp" class="font-mono font-bold">051234</span></p>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="text-center">
|
| 148 |
+
<p class="text-sm text-gray-600">Holder: <span id="preview-name" class="font-medium">John Doe</span></p>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div id="actions" class="hidden">
|
| 154 |
+
<div class="grid grid-cols-2 gap-4">
|
| 155 |
+
<button id="download-btn" class="bg-white border border-purple-600 text-purple-600 py-2 rounded-lg hover:bg-purple-50 transition duration-300 flex items-center justify-center">
|
| 156 |
+
<i data-feather="download" class="w-4 h-4 mr-2"></i>
|
| 157 |
+
Download
|
| 158 |
+
</button>
|
| 159 |
+
<button id="print-btn" class="bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition duration-300 flex items-center justify-center">
|
| 160 |
+
<i data-feather="printer" class="w-4 h-4 mr-2"></i>
|
| 161 |
+
Print
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<div class="mt-4">
|
| 166 |
+
<button id="new-ticket-btn" class="w-full bg-white border border-gray-300 text-gray-700 py-2 rounded-lg hover:bg-gray-50 transition duration-300 flex items-center justify-center">
|
| 167 |
+
<i data-feather="plus" class="w-4 h-4 mr-2"></i>
|
| 168 |
+
Generate Another
|
| 169 |
+
</button>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</main>
|
| 176 |
+
|
| 177 |
+
<script>
|
| 178 |
+
feather.replace();
|
| 179 |
+
|
| 180 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 181 |
+
const form = document.getElementById('ticket-form');
|
| 182 |
+
const emptyState = document.getElementById('empty-state');
|
| 183 |
+
const ticketContent = document.getElementById('ticket-content');
|
| 184 |
+
const actions = document.getElementById('actions');
|
| 185 |
+
const qrCodeDiv = document.getElementById('qr-code');
|
| 186 |
+
const newTicketBtn = document.getElementById('new-ticket-btn');
|
| 187 |
+
|
| 188 |
+
// Event data
|
| 189 |
+
const eventData = {
|
| 190 |
+
'05': { name: 'Music Festival', date: 'October 25, 2023' },
|
| 191 |
+
'12': { name: 'Tech Conference', date: 'November 15, 2023' },
|
| 192 |
+
'18': { name: 'Art Exhibition', date: 'December 5, 2023' },
|
| 193 |
+
'23': { name: 'Sports Game', date: 'January 20, 2024' }
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// Ticket type colors
|
| 197 |
+
const typeColors = {
|
| 198 |
+
'general': { bg: 'purple-100', text: 'purple-700' },
|
| 199 |
+
'vip': { bg: 'gold-100', text: 'gold-700' },
|
| 200 |
+
'student': { bg: 'blue-100', text: 'blue-700' },
|
| 201 |
+
'earlybird': { bg: 'green-100', text: 'green-700' }
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
// Generate ticket
|
| 205 |
+
form.addEventListener('submit', function(e) {
|
| 206 |
+
e.preventDefault();
|
| 207 |
+
|
| 208 |
+
const eventId = document.getElementById('event-select').value;
|
| 209 |
+
const ticketType = document.getElementById('ticket-type').value;
|
| 210 |
+
const attendeeName = document.getElementById('attendee-name').value;
|
| 211 |
+
const ticketCount = document.getElementById('ticket-count').value;
|
| 212 |
+
|
| 213 |
+
if (!eventId) {
|
| 214 |
+
alert('Please select an event');
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
if (!attendeeName) {
|
| 219 |
+
alert('Please enter attendee name');
|
| 220 |
+
return;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Generate random 4-digit code
|
| 224 |
+
const userCode = Math.floor(1000 + Math.random() * 9000);
|
| 225 |
+
const otpCode = eventId + userCode;
|
| 226 |
+
|
| 227 |
+
// Update preview
|
| 228 |
+
document.getElementById('preview-event').textContent = eventData[eventId].name;
|
| 229 |
+
document.getElementById('preview-date').textContent = eventData[eventId].date;
|
| 230 |
+
document.getElementById('preview-name').textContent = attendeeName;
|
| 231 |
+
document.getElementById('preview-otp').textContent = otpCode;
|
| 232 |
+
|
| 233 |
+
// Update ticket type badge
|
| 234 |
+
const typeBadge = document.getElementById('preview-type');
|
| 235 |
+
typeBadge.className = `bg-${typeColors[ticketType].bg} px-3 py-1 rounded-full`;
|
| 236 |
+
typeBadge.innerHTML = `<span class="text-${typeColors[ticketType].text} text-sm font-medium capitalize">${ticketType}</span>`;
|
| 237 |
+
|
| 238 |
+
// Generate QR code
|
| 239 |
+
qrCodeDiv.innerHTML = '';
|
| 240 |
+
const qr = qrcode(0, 'L');
|
| 241 |
+
qr.addData(otpCode);
|
| 242 |
+
qr.make();
|
| 243 |
+
qrCodeDiv.innerHTML = qr.createImgTag(4);
|
| 244 |
+
|
| 245 |
+
// Show ticket and actions
|
| 246 |
+
emptyState.classList.add('hidden');
|
| 247 |
+
ticketContent.classList.remove('hidden');
|
| 248 |
+
actions.classList.remove('hidden');
|
| 249 |
+
|
| 250 |
+
// Scroll to preview
|
| 251 |
+
ticketContent.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 252 |
+
});
|
| 253 |
+
|
| 254 |
+
// New ticket button
|
| 255 |
+
newTicketBtn.addEventListener('click', function() {
|
| 256 |
+
emptyState.classList.remove('hidden');
|
| 257 |
+
ticketContent.classList.add('hidden');
|
| 258 |
+
actions.classList.add('hidden');
|
| 259 |
+
form.reset();
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
// Download button
|
| 263 |
+
document.getElementById('download-btn').addEventListener('click', function() {
|
| 264 |
+
alert('Ticket downloaded! (This would download the QR code and ticket details in a real implementation)');
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
// Print button
|
| 268 |
+
document.getElementById('print-btn').addEventListener('click', function() {
|
| 269 |
+
window.print();
|
| 270 |
+
});
|
| 271 |
+
});
|
| 272 |
+
</script>
|
| 273 |
+
</body>
|
| 274 |
+
</html>
|
|
@@ -1,19 +1,258 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>QRush - Event Ticketing System</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
.gradient-bg {
|
| 13 |
+
background: linear-gradient(135deg, #6e8efb 0%, #a777e3 100%);
|
| 14 |
+
}
|
| 15 |
+
.qr-scanner {
|
| 16 |
+
width: 100%;
|
| 17 |
+
max-width: 500px;
|
| 18 |
+
position: relative;
|
| 19 |
+
border-radius: 1rem;
|
| 20 |
+
overflow: hidden;
|
| 21 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
| 22 |
+
}
|
| 23 |
+
.scanner-overlay {
|
| 24 |
+
position: absolute;
|
| 25 |
+
top: 0;
|
| 26 |
+
left: 0;
|
| 27 |
+
width: 100%;
|
| 28 |
+
height: 100%;
|
| 29 |
+
border: 2px dashed rgba(255, 255, 255, 0.7);
|
| 30 |
+
pointer-events: none;
|
| 31 |
+
}
|
| 32 |
+
.ticket-card {
|
| 33 |
+
transition: all 0.3s ease;
|
| 34 |
+
transform-style: preserve-3d;
|
| 35 |
+
}
|
| 36 |
+
.ticket-card:hover {
|
| 37 |
+
transform: translateY(-5px) rotateX(5deg);
|
| 38 |
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
| 39 |
+
}
|
| 40 |
+
</style>
|
| 41 |
+
</head>
|
| 42 |
+
<body class="min-h-screen bg-gray-50">
|
| 43 |
+
<!-- Navigation -->
|
| 44 |
+
<nav class="gradient-bg text-white shadow-lg">
|
| 45 |
+
<div class="container mx-auto px-6 py-3 flex justify-between items-center">
|
| 46 |
+
<div class="flex items-center space-x-4">
|
| 47 |
+
<i data-feather="qr-code" class="w-8 h-8"></i>
|
| 48 |
+
<span class="text-xl font-bold">QRush</span>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="hidden md:flex space-x-8">
|
| 51 |
+
<a href="#" class="hover:text-purple-200 font-medium">Events</a>
|
| 52 |
+
<a href="#" class="hover:text-purple-200 font-medium">Buy Tickets</a>
|
| 53 |
+
<a href="#" class="hover:text-purple-200 font-medium">Validate</a>
|
| 54 |
+
<a href="#" class="hover:text-purple-200 font-medium">Admin</a>
|
| 55 |
+
</div>
|
| 56 |
+
<button class="md:hidden focus:outline-none">
|
| 57 |
+
<i data-feather="menu" class="w-6 h-6"></i>
|
| 58 |
+
</button>
|
| 59 |
+
</div>
|
| 60 |
+
</nav>
|
| 61 |
+
|
| 62 |
+
<!-- Hero Section -->
|
| 63 |
+
<section class="gradient-bg text-white py-20">
|
| 64 |
+
<div class="container mx-auto px-6 text-center">
|
| 65 |
+
<h1 class="text-5xl font-bold mb-6">Your Event, Simplified</h1>
|
| 66 |
+
<p class="text-xl mb-8 max-w-2xl mx-auto">QR-based ticketing system that makes event access seamless for everyone.</p>
|
| 67 |
+
<div class="flex justify-center space-x-4">
|
| 68 |
+
<button class="bg-white text-purple-700 px-6 py-3 rounded-full font-semibold hover:bg-purple-100 transition duration-300">
|
| 69 |
+
Create Event
|
| 70 |
+
</button>
|
| 71 |
+
<button class="border-2 border-white text-white px-6 py-3 rounded-full font-semibold hover:bg-white hover:text-purple-700 transition duration-300">
|
| 72 |
+
Learn More
|
| 73 |
+
</button>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</section>
|
| 77 |
+
|
| 78 |
+
<!-- Features Section -->
|
| 79 |
+
<section class="py-16 bg-white">
|
| 80 |
+
<div class="container mx-auto px-6">
|
| 81 |
+
<h2 class="text-3xl font-bold text-center mb-16 text-gray-800">How It Works</h2>
|
| 82 |
+
<div class="grid md:grid-cols-3 gap-10">
|
| 83 |
+
<!-- Feature 1 -->
|
| 84 |
+
<div class="text-center px-6">
|
| 85 |
+
<div class="bg-purple-100 w-20 h-20 mx-auto rounded-full flex items-center justify-center mb-4">
|
| 86 |
+
<i data-feather="ticket" class="w-10 h-10 text-purple-700"></i>
|
| 87 |
+
</div>
|
| 88 |
+
<h3 class="text-xl font-semibold mb-3 text-gray-800">Generate Tickets</h3>
|
| 89 |
+
<p class="text-gray-600">Create unique QR code tickets with numeric OTP fallback in seconds.</p>
|
| 90 |
+
</div>
|
| 91 |
+
<!-- Feature 2 -->
|
| 92 |
+
<div class="text-center px-6">
|
| 93 |
+
<div class="bg-blue-100 w-20 h-20 mx-auto rounded-full flex items-center justify-center mb-4">
|
| 94 |
+
<i data-feather="smartphone" class="w-10 h-10 text-blue-700"></i>
|
| 95 |
+
</div>
|
| 96 |
+
<h3 class="text-xl font-semibold mb-3 text-gray-800">Easy Validation</h3>
|
| 97 |
+
<p class="text-gray-600">Scan QR codes or manually enter OTPs for fast, reliable validation.</p>
|
| 98 |
+
</div>
|
| 99 |
+
<!-- Feature 3 -->
|
| 100 |
+
<div class="text-center px-6">
|
| 101 |
+
<div class="bg-green-100 w-20 h-20 mx-auto rounded-full flex items-center justify-center mb-4">
|
| 102 |
+
<i data-feather="shield" class="w-10 h-10 text-green-700"></i>
|
| 103 |
+
</div>
|
| 104 |
+
<h3 class="text-xl font-semibold mb-3 text-gray-800">Secure System</h3>
|
| 105 |
+
<p class="text-gray-600">One-time use tickets prevent fraud and ensure event security.</p>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</section>
|
| 110 |
+
|
| 111 |
+
<!-- Demo Section -->
|
| 112 |
+
<section class="py-16 bg-gray-50">
|
| 113 |
+
<div class="container mx-auto px-6">
|
| 114 |
+
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">Try It Out</h2>
|
| 115 |
+
<div class="flex flex-col md:flex-row items-center justify-center gap-10">
|
| 116 |
+
<!-- QR Scanner Demo -->
|
| 117 |
+
<div class="w-full md:w-1/2">
|
| 118 |
+
<div class="bg-white p-8 rounded-xl shadow-lg">
|
| 119 |
+
<h3 class="text-xl font-semibold mb-4 text-gray-800">QR Scanner</h3>
|
| 120 |
+
<div class="qr-scanner bg-black mb-4">
|
| 121 |
+
<video id="scanner-video" class="w-full" autoplay playsinline></video>
|
| 122 |
+
<div class="scanner-overlay"></div>
|
| 123 |
+
</div>
|
| 124 |
+
<p class="text-gray-600 mb-4">Or enter OTP manually:</p>
|
| 125 |
+
<div class="flex">
|
| 126 |
+
<input type="text" 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">
|
| 127 |
+
<button class="bg-purple-600 text-white px-4 py-2 rounded-r-lg hover:bg-purple-700 transition duration-300">Validate</button>
|
| 128 |
+
</div>
|
| 129 |
+
<div id="validation-result" class="mt-4 hidden">
|
| 130 |
+
<div class="flex items-center">
|
| 131 |
+
<i data-feather="check-circle" class="w-6 h-6 text-green-500 mr-2"></i>
|
| 132 |
+
<span class="text-green-600 font-medium">Valid Ticket</span>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="mt-2 text-sm text-gray-600">
|
| 135 |
+
Event: <span class="font-medium">Music Festival</span><br>
|
| 136 |
+
Holder: <span class="font-medium">John Doe</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<!-- Ticket Demo -->
|
| 143 |
+
<div class="w-full md:w-1/2">
|
| 144 |
+
<div class="bg-white p-8 rounded-xl shadow-lg">
|
| 145 |
+
<h3 class="text-xl font-semibold mb-4 text-gray-800">Sample Ticket</h3>
|
| 146 |
+
<div class="ticket-card bg-gradient-to-r from-purple-50 to-blue-50 p-6 rounded-lg border border-purple-100 mb-4">
|
| 147 |
+
<div class="flex justify-between items-start mb-4">
|
| 148 |
+
<div>
|
| 149 |
+
<h4 class="font-bold text-lg text-gray-800">Music Festival</h4>
|
| 150 |
+
<p class="text-sm text-gray-600">October 25, 2023</p>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="bg-purple-100 px-3 py-1 rounded-full">
|
| 153 |
+
<span class="text-purple-700 text-sm font-medium">VIP</span>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="flex flex-col items-center mb-4">
|
| 157 |
+
<div id="demo-qr" class="w-40 h-40 mb-3 bg-white p-2 rounded"></div>
|
| 158 |
+
<p class="text-sm text-gray-600">OTP: <span class="font-mono font-bold">051234</span></p>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="text-center">
|
| 161 |
+
<p class="text-sm text-gray-600">Holder: <span class="font-medium">John Doe</span></p>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
<button class="w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition duration-300">
|
| 165 |
+
Generate New Ticket
|
| 166 |
+
</button>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</section>
|
| 172 |
+
|
| 173 |
+
<!-- Footer -->
|
| 174 |
+
<footer class="bg-gray-800 text-white py-12">
|
| 175 |
+
<div class="container mx-auto px-6">
|
| 176 |
+
<div class="grid md:grid-cols-4 gap-8">
|
| 177 |
+
<div>
|
| 178 |
+
<h3 class="text-xl font-bold mb-4 flex items-center">
|
| 179 |
+
<i data-feather="qr-code" class="w-6 h-6 mr-2"></i>
|
| 180 |
+
QRush
|
| 181 |
+
</h3>
|
| 182 |
+
<p class="text-gray-400">Making event ticketing simple, secure, and seamless.</p>
|
| 183 |
+
</div>
|
| 184 |
+
<div>
|
| 185 |
+
<h4 class="font-semibold mb-4">Product</h4>
|
| 186 |
+
<ul class="space-y-2">
|
| 187 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Features</a></li>
|
| 188 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Pricing</a></li>
|
| 189 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">API</a></li>
|
| 190 |
+
</ul>
|
| 191 |
+
</div>
|
| 192 |
+
<div>
|
| 193 |
+
<h4 class="font-semibold mb-4">Resources</h4>
|
| 194 |
+
<ul class="space-y-2">
|
| 195 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Documentation</a></li>
|
| 196 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Guides</a></li>
|
| 197 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Support</a></li>
|
| 198 |
+
</ul>
|
| 199 |
+
</div>
|
| 200 |
+
<div>
|
| 201 |
+
<h4 class="font-semibold mb-4">Company</h4>
|
| 202 |
+
<ul class="space-y-2">
|
| 203 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">About</a></li>
|
| 204 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Blog</a></li>
|
| 205 |
+
<li><a href="#" class="text-gray-400 hover:text-white transition">Contact</a></li>
|
| 206 |
+
</ul>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="border-t border-gray-700 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center">
|
| 210 |
+
<p class="text-gray-400 mb-4 md:mb-0">© 2023 QRush. All rights reserved.</p>
|
| 211 |
+
<div class="flex space-x-4">
|
| 212 |
+
<a href="#" class="text-gray-400 hover:text-white transition"><i data-feather="twitter"></i></a>
|
| 213 |
+
<a href="#" class="text-gray-400 hover:text-white transition"><i data-feather="facebook"></i></a>
|
| 214 |
+
<a href="#" class="text-gray-400 hover:text-white transition"><i data-feather="instagram"></i></a>
|
| 215 |
+
<a href="#" class="text-gray-400 hover:text-white transition"><i data-feather="github"></i></a>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</footer>
|
| 220 |
+
|
| 221 |
+
<script>
|
| 222 |
+
// Initialize Feather Icons
|
| 223 |
+
feather.replace();
|
| 224 |
+
|
| 225 |
+
// Generate sample QR code
|
| 226 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 227 |
+
const qr = qrcode(0, 'L');
|
| 228 |
+
qr.addData('051234');
|
| 229 |
+
qr.make();
|
| 230 |
+
document.getElementById('demo-qr').innerHTML = qr.createImgTag(4);
|
| 231 |
+
|
| 232 |
+
// Simulate scanner (placeholder for actual implementation)
|
| 233 |
+
document.querySelector('button').addEventListener('click', function() {
|
| 234 |
+
const resultDiv = document.getElementById('validation-result');
|
| 235 |
+
resultDiv.classList.remove('hidden');
|
| 236 |
+
|
| 237 |
+
// Hide after 3 seconds
|
| 238 |
+
setTimeout(() => {
|
| 239 |
+
resultDiv.classList.add('hidden');
|
| 240 |
+
}, 3000);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
// Initialize camera for scanner demo
|
| 244 |
+
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
| 245 |
+
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
| 246 |
+
.then(function(stream) {
|
| 247 |
+
const video = document.getElementById('scanner-video');
|
| 248 |
+
video.srcObject = stream;
|
| 249 |
+
video.play();
|
| 250 |
+
})
|
| 251 |
+
.catch(function(err) {
|
| 252 |
+
console.error("Camera access error:", err);
|
| 253 |
+
});
|
| 254 |
+
}
|
| 255 |
+
});
|
| 256 |
+
</script>
|
| 257 |
+
</body>
|
| 258 |
</html>
|
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Validate Tickets | QRush</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.min.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
.scanner-container {
|
| 12 |
+
position: relative;
|
| 13 |
+
width: 100%;
|
| 14 |
+
max-width: 600px;
|
| 15 |
+
margin: 0 auto;
|
| 16 |
+
}
|
| 17 |
+
.scanner-video {
|
| 18 |
+
width: 100%;
|
| 19 |
+
border-radius: 0.75rem;
|
| 20 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 21 |
+
}
|
| 22 |
+
.scanner-overlay {
|
| 23 |
+
position: absolute;
|
| 24 |
+
top: 0;
|
| 25 |
+
left: 0;
|
| 26 |
+
width: 100%;
|
| 27 |
+
height: 100%;
|
| 28 |
+
border: 3px dashed rgba(255, 255, 255, 0.7);
|
| 29 |
+
pointer-events: none;
|
| 30 |
+
}
|
| 31 |
+
.validation-result {
|
| 32 |
+
transition: all 0.3s ease;
|
| 33 |
+
}
|
| 34 |
+
</style>
|
| 35 |
+
</head>
|
| 36 |
+
<body class="min-h-screen bg-gray-100">
|
| 37 |
+
<!-- Navigation -->
|
| 38 |
+
<nav class="bg-white shadow-sm">
|
| 39 |
+
<div class="container mx-auto px-6 py-3 flex justify-between items-center">
|
| 40 |
+
<div class="flex items-center space-x-4">
|
| 41 |
+
<i data-feather="qr-code" class="w-6 h-6 text-purple-600"></i>
|
| 42 |
+
<span class="text-xl font-bold text-gray-800">QRush</span>
|
| 43 |
+
</div>
|
| 44 |
+
<a href="index.html" class="text-gray-600 hover:text-purple-600 flex items-center">
|
| 45 |
+
<i data-feather="home" class="w-5 h-5 mr-1"></i>
|
| 46 |
+
Back to Home
|
| 47 |
+
</a>
|
| 48 |
+
</div>
|
| 49 |
+
</nav>
|
| 50 |
+
|
| 51 |
+
<!-- Main Content -->
|
| 52 |
+
<main class="container mx-auto px-6 py-12">
|
| 53 |
+
<div class="text-center mb-12">
|
| 54 |
+
<h1 class="text-3xl font-bold text-gray-800 mb-3">Ticket Validation</h1>
|
| 55 |
+
<p class="text-gray-600 max-w-2xl mx-auto">Scan QR codes or manually enter OTP to validate event tickets.</p>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="flex flex-col lg:flex-row gap-10">
|
| 59 |
+
<!-- Scanner Section -->
|
| 60 |
+
<div class="w-full lg:w-1/2">
|
| 61 |
+
<div class="bg-white p-6 rounded-xl shadow-md">
|
| 62 |
+
<div class="flex justify-between items-center mb-4">
|
| 63 |
+
<h2 class="text-xl font-semibold text-gray-800">QR Scanner</h2>
|
| 64 |
+
<button id="toggle-camera" class="text-purple-600 hover:text-purple-800 flex items-center">
|
| 65 |
+
<i data-feather="refresh-cw" class="w-4 h-4 mr-1"></i>
|
| 66 |
+
Switch Camera
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="scanner-container bg-black rounded-lg overflow-hidden mb-4">
|
| 71 |
+
<video id="scanner-video" class="scanner-video" autoplay playsinline></video>
|
| 72 |
+
<div class="scanner-overlay"></div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div class="flex items-center justify-center mb-4">
|
| 76 |
+
<div class="h-px bg-gray-200 flex-1"></div>
|
| 77 |
+
<span class="mx-4 text-gray-500 text-sm">OR</span>
|
| 78 |
+
<div class="h-px bg-gray-200 flex-1"></div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div class="mb-4">
|
| 82 |
+
<label for="manual-code" class="block text-sm font-medium text-gray-700 mb-2">Enter OTP Manually</label>
|
| 83 |
+
<div class="flex">
|
| 84 |
+
<input
|
| 85 |
+
type="text"
|
| 86 |
+
id="manual-code"
|
| 87 |
+
placeholder="Enter 6-digit code"
|
| 88 |
+
class="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
| 89 |
+
maxlength="6"
|
| 90 |
+
pattern="\d{6}"
|
| 91 |
+
title="Please enter a 6-digit code"
|
| 92 |
+
>
|
| 93 |
+
<button id="validate-btn" class="bg-purple-600 text-white px-4 py-2 rounded-r-lg hover:bg-purple-700 transition duration-300">
|
| 94 |
+
Validate
|
| 95 |
+
</button>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- Results Section -->
|
| 102 |
+
<div class="w-full lg:w-1/2">
|
| 103 |
+
<div class="bg-white p-6 rounded-xl shadow-md h-full">
|
| 104 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">Validation Results</h2>
|
| 105 |
+
|
| 106 |
+
<div id="validation-result" class="validation-result hidden">
|
| 107 |
+
<div id="valid-ticket" class="hidden">
|
| 108 |
+
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
|
| 109 |
+
<div class="flex items-center">
|
| 110 |
+
<i data-feather="check-circle" class="w-6 h-6 text-green-500 mr-2"></i>
|
| 111 |
+
<span class="text-green-700 font-medium">Valid Ticket</span>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
| 115 |
+
<div class="grid grid-cols-2 gap-4 mb-4">
|
| 116 |
+
<div>
|
| 117 |
+
<p class="text-sm text-gray-500">Event ID</p>
|
| 118 |
+
<p id="event-id" class="font-medium">05</p>
|
| 119 |
+
</div>
|
| 120 |
+
<div>
|
| 121 |
+
<p class="text-sm text-gray-500">OTP Code</p>
|
| 122 |
+
<p id="otp-code" class="font-mono font-medium">051234</p>
|
| 123 |
+
</div>
|
| 124 |
+
<div>
|
| 125 |
+
<p class="text-sm text-gray-500">Ticket Holder</p>
|
| 126 |
+
<p id="ticket-holder" class="font-medium">John Doe</p>
|
| 127 |
+
</div>
|
| 128 |
+
<div>
|
| 129 |
+
<p class="text-sm text-gray-500">Status</p>
|
| 130 |
+
<p id="ticket-status" class="font-medium">Valid</p>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="text-center">
|
| 134 |
+
<button class="bg-green-100 text-green-700 px-4 py-2 rounded-lg text-sm font-medium">
|
| 135 |
+
<i data-feather="check" class="w-4 h-4 inline mr-1"></i>
|
| 136 |
+
Entry Approved
|
| 137 |
+
</button>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div id="invalid-ticket" class="hidden">
|
| 143 |
+
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
| 144 |
+
<div class="flex items-center">
|
| 145 |
+
<i data-feather="x-circle" class="w-6 h-6 text-red-500 mr-2"></i>
|
| 146 |
+
<span class="text-red-700 font-medium">Invalid Ticket</span>
|
| 147 |
+
</div>
|
| 148 |
+
<p class="mt-2 text-sm text-gray-600" id="invalid-reason">This ticket has already been used or doesn't exist.</p>
|
| 149 |
+
</div>
|
| 150 |
+
<div class="mt-4 text-center">
|
| 151 |
+
<button class="bg-red-100 text-red-700 px-4 py-2 rounded-lg text-sm font-medium">
|
| 152 |
+
<i data-feather="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
| 153 |
+
Deny Entry
|
| 154 |
+
</button>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div id="no-result" class="text-center py-12">
|
| 160 |
+
<div class="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
| 161 |
+
<i data-feather="search" class="w-8 h-8 text-gray-400"></i>
|
| 162 |
+
</div>
|
| 163 |
+
<h3 class="text-lg font-medium text-gray-500 mb-2">No ticket scanned yet</h3>
|
| 164 |
+
<p class="text-gray-400 text-sm">Scan a QR code or enter OTP manually to validate a ticket.</p>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div class="mt-6 border-t border-gray-200 pt-4">
|
| 168 |
+
<h3 class="text-sm font-medium text-gray-500 mb-2">Recent Validations</h3>
|
| 169 |
+
<div class="space-y-2">
|
| 170 |
+
<div class="flex justify-between text-sm">
|
| 171 |
+
<span class="text-gray-700">051234</span>
|
| 172 |
+
<span class="text-green-600">Valid</span>
|
| 173 |
+
</div>
|
| 174 |
+
<div class="flex justify-between text-sm">
|
| 175 |
+
<span class="text-gray-700">056789</span>
|
| 176 |
+
<span class="text-red-600">Invalid</span>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="flex justify-between text-sm">
|
| 179 |
+
<span class="text-gray-700">052345</span>
|
| 180 |
+
<span class="text-green-600">Valid</span>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</main>
|
| 188 |
+
|
| 189 |
+
<script>
|
| 190 |
+
feather.replace();
|
| 191 |
+
|
| 192 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 193 |
+
const video = document.getElementById('scanner-video');
|
| 194 |
+
const validateBtn = document.getElementById('validate-btn');
|
| 195 |
+
const manualCodeInput = document.getElementById('manual-code');
|
| 196 |
+
const validationResult = document.getElementById('validation-result');
|
| 197 |
+
const validTicket = document.getElementById('valid-ticket');
|
| 198 |
+
const invalidTicket = document.getElementById('invalid-ticket');
|
| 199 |
+
const noResult = document.getElementById('no-result');
|
| 200 |
+
const toggleCameraBtn = document.getElementById('toggle-camera');
|
| 201 |
+
|
| 202 |
+
let currentFacingMode = 'environment';
|
| 203 |
+
let stream = null;
|
| 204 |
+
|
| 205 |
+
// Initialize scanner
|
| 206 |
+
function initScanner(facingMode) {
|
| 207 |
+
stopScanner();
|
| 208 |
+
|
| 209 |
+
navigator.mediaDevices.getUserMedia({
|
| 210 |
+
video: {
|
| 211 |
+
facingMode: facingMode,
|
| 212 |
+
width: { ideal: 1280 },
|
| 213 |
+
height: { ideal: 720 }
|
| 214 |
+
},
|
| 215 |
+
audio: false
|
| 216 |
+
}).then(function(s) {
|
| 217 |
+
stream = s;
|
| 218 |
+
video.srcObject = stream;
|
| 219 |
+
video.play();
|
| 220 |
+
|
| 221 |
+
// Start scanning
|
| 222 |
+
scanQRCode();
|
| 223 |
+
}).catch(function(err) {
|
| 224 |
+
console.error("Camera error:", err);
|
| 225 |
+
});
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// Stop scanner
|
| 229 |
+
function stopScanner() {
|
| 230 |
+
if (stream) {
|
| 231 |
+
stream.getTracks().forEach(track => track.stop());
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// Scan QR code from video stream
|
| 236 |
+
function scanQRCode() {
|
| 237 |
+
const canvas = document.createElement('canvas');
|
| 238 |
+
const context = canvas.getContext('2d');
|
| 239 |
+
|
| 240 |
+
function tick() {
|
| 241 |
+
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
| 242 |
+
canvas.width = video.videoWidth;
|
| 243 |
+
canvas.height = video.videoHeight;
|
| 244 |
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
| 245 |
+
|
| 246 |
+
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
| 247 |
+
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
| 248 |
+
|
| 249 |
+
if (code) {
|
| 250 |
+
validateTicket(code.data);
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
requestAnimationFrame(tick);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
tick();
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// Validate ticket (mock function)
|
| 261 |
+
function validateTicket(code) {
|
| 262 |
+
// Show loading state
|
| 263 |
+
validationResult.classList.remove('hidden');
|
| 264 |
+
validTicket.classList.add('hidden');
|
| 265 |
+
invalidTicket.classList.add('hidden');
|
| 266 |
+
noResult.classList.add('hidden');
|
| 267 |
+
|
| 268 |
+
// Simulate API call
|
| 269 |
+
setTimeout(() => {
|
| 270 |
+
// Mock validation - odd numbers are valid, even are invalid
|
| 271 |
+
const isValid = parseInt(code) % 2 !== 0;
|
| 272 |
+
|
| 273 |
+
if (isValid) {
|
| 274 |
+
document.getElementById('event-id').textContent = code.substring(0, 2);
|
| 275 |
+
document.getElementById('otp-code').textContent = code;
|
| 276 |
+
document.getElementById('ticket-holder').textContent = "Ticket Holder " + code.substring(2);
|
| 277 |
+
document.getElementById('ticket-status').textContent = "Valid";
|
| 278 |
+
|
| 279 |
+
validTicket.classList.remove('hidden');
|
| 280 |
+
invalidTicket.classList.add('hidden');
|
| 281 |
+
} else {
|
| 282 |
+
document.getElementById('invalid-reason').textContent =
|
| 283 |
+
code.length === 6 ? "This ticket has already been used." : "Invalid code format.";
|
| 284 |
+
|
| 285 |
+
validTicket.classList.add('hidden');
|
| 286 |
+
invalidTicket.classList.remove('hidden');
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
noResult.classList.add('hidden');
|
| 290 |
+
}, 800);
|
| 291 |
+
|
| 292 |
+
// Clear manual input
|
| 293 |
+
manualCodeInput.value = '';
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Toggle camera
|
| 297 |
+
toggleCameraBtn.addEventListener('click', function() {
|
| 298 |
+
currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment';
|
| 299 |
+
initScanner(currentFacingMode);
|
| 300 |
+
});
|
| 301 |
+
|
| 302 |
+
// Manual validation
|
| 303 |
+
validateBtn.addEventListener('click', function() {
|
| 304 |
+
const code = manualCodeInput.value.trim();
|
| 305 |
+
if (code.length === 6 && /^\d+$/.test(code)) {
|
| 306 |
+
validateTicket(code);
|
| 307 |
+
} else {
|
| 308 |
+
alert("Please enter a valid 6-digit code");
|
| 309 |
+
}
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
// Initialize with back camera
|
| 313 |
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
| 314 |
+
initScanner(currentFacingMode);
|
| 315 |
+
} else {
|
| 316 |
+
alert("Camera access not available in your browser.");
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// Clean up on page leave
|
| 320 |
+
window.addEventListener('beforeunload', function() {
|
| 321 |
+
stopScanner();
|
| 322 |
+
});
|
| 323 |
+
});
|
| 324 |
+
</script>
|
| 325 |
+
</body>
|
| 326 |
+
</html>
|