qr code - responsive
This commit is contained in:
@@ -18,40 +18,40 @@ export const Layout: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<nav className="glass-panel sticky top-0 z-50 border-x-0 border-t-0 rounded-none">
|
||||
<div className="container py-4 flex justify-between items-center">
|
||||
<Link to="/" className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400 flex items-center gap-2">
|
||||
<QrCode className="text-indigo-400" />
|
||||
EventQR
|
||||
<div className="container py-3 md:py-4 flex justify-between items-center">
|
||||
<Link to="/" className="text-xl md:text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400 flex items-center gap-2">
|
||||
<QrCode className="text-indigo-400" size={24} />
|
||||
<span className="hidden xs:inline">EventQR</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-6 items-center">
|
||||
<div className="flex gap-3 md:gap-6 items-center">
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex items-center gap-2 hover:text-indigo-400 transition-colors ${isActive('/') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
className={`flex items-center gap-1 md:gap-2 hover:text-indigo-400 transition-colors ${isActive('/') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
>
|
||||
<Calendar size={20} />
|
||||
<span className="hidden sm:inline">Events</span>
|
||||
<span className="hidden sm:inline text-sm md:text-base">Events</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/generate"
|
||||
className={`flex items-center gap-2 hover:text-indigo-400 transition-colors ${isActive('/generate') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
className={`flex items-center gap-1 md:gap-2 hover:text-indigo-400 transition-colors ${isActive('/generate') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
>
|
||||
<Mail size={20} />
|
||||
<span className="hidden sm:inline">Generate</span>
|
||||
<span className="hidden sm:inline text-sm md:text-base">Generate</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/scan"
|
||||
className={`flex items-center gap-2 hover:text-indigo-400 transition-colors ${isActive('/scan') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
className={`flex items-center gap-1 md:gap-2 hover:text-indigo-400 transition-colors ${isActive('/scan') ? 'text-indigo-400' : 'text-slate-400'}`}
|
||||
>
|
||||
<Scan size={20} />
|
||||
<span className="hidden sm:inline">Scan</span>
|
||||
<span className="hidden sm:inline text-sm md:text-base">Scan</span>
|
||||
</Link>
|
||||
|
||||
<div className="w-px h-6 bg-slate-700 mx-2"></div>
|
||||
<div className="w-px h-6 bg-slate-700 mx-1 md:mx-2"></div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 text-slate-400 hover:text-red-400 transition-colors"
|
||||
className="flex items-center gap-1 md:gap-2 text-slate-400 hover:text-red-400 transition-colors"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
@@ -60,11 +60,11 @@ export const Layout: React.FC = () => {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="flex-1 container py-8 page-transition">
|
||||
<main className="flex-1 container py-6 md:py-8 page-transition">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<footer className="py-6 text-center text-slate-600 text-sm">
|
||||
<footer className="py-4 md:py-6 text-center text-slate-600 text-xs md:text-sm">
|
||||
<p>© 2024 EventQR Tool. Built for seamless event management.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -55,13 +55,21 @@ button {
|
||||
.btn-primary {
|
||||
background: var(--accent-gradient);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
@@ -70,14 +78,31 @@ button {
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--glass-border);
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
@@ -90,13 +115,22 @@ textarea {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-primary);
|
||||
padding: 0.75rem;
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
@@ -107,10 +141,22 @@ textarea:focus {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-transition {
|
||||
/* animation: fadeIn 0.3s ease-in-out; */
|
||||
}
|
||||
@@ -148,11 +194,14 @@ textarea:focus {
|
||||
/* Scanner Overrides */
|
||||
#reader {
|
||||
border: none !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
#reader video {
|
||||
border-radius: 0.5rem;
|
||||
object-fit: cover;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#reader__scan_region {
|
||||
@@ -167,6 +216,13 @@ textarea:focus {
|
||||
border-radius: 0.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
margin-top: 1rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#reader__dashboard_section_csr button {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#reader__dashboard_section_swaplink {
|
||||
@@ -174,4 +230,11 @@ textarea:focus {
|
||||
text-decoration: none !important;
|
||||
margin-top: 0.5rem !important;
|
||||
display: inline-block !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#reader__dashboard_section_swaplink {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ export const Generate: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">Generate Ticket</h1>
|
||||
<p className="text-slate-400">Create a new QR code ticket for an attendee.</p>
|
||||
<h1 className="text-2xl md:text-3xl font-bold mb-2">Generate Ticket</h1>
|
||||
<p className="text-slate-400 text-sm md:text-base">Create a new QR code ticket for an attendee.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="glass-panel p-6 space-y-4">
|
||||
<form onSubmit={handleSubmit} className="glass-panel p-4 md:p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-slate-300">Attendee Name</label>
|
||||
<input
|
||||
@@ -111,28 +111,29 @@ export const Generate: React.FC = () => {
|
||||
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{generatedTicket ? (
|
||||
<div className="glass-panel p-8 text-center w-full max-w-sm animate-in fade-in zoom-in duration-300">
|
||||
<div className="bg-green-500/10 text-green-400 p-3 rounded-lg mb-6 text-sm font-medium">
|
||||
<div className="glass-panel p-6 md:p-8 text-center w-full max-w-sm animate-in fade-in zoom-in duration-300">
|
||||
<div className="bg-green-500/10 text-green-400 p-3 rounded-lg mb-4 md:mb-6 text-xs md:text-sm font-medium">
|
||||
Ticket generated successfully!
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg inline-block mb-6">
|
||||
|
||||
<div className="bg-white p-3 md:p-4 rounded-lg inline-block mb-4 md:mb-6">
|
||||
<QRCodeSVG
|
||||
value={JSON.stringify({ id: generatedTicket.id, type: generatedTicket.ticketType })}
|
||||
size={200}
|
||||
size={180}
|
||||
level="H"
|
||||
className="md:w-[200px] md:h-[200px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mb-1">{generatedTicket.attendeeName}</h3>
|
||||
<p className="text-slate-400 text-sm mb-6">{generatedTicket.ticketType} Ticket</p>
|
||||
<h3 className="text-lg md:text-xl font-bold mb-1">{generatedTicket.attendeeName}</h3>
|
||||
<p className="text-slate-400 text-xs md:text-sm mb-4 md:mb-6">{generatedTicket.ticketType} Ticket</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
<button
|
||||
onClick={handleResendEmail}
|
||||
disabled={resendStatus === 'sending' || resendStatus === 'sent'}
|
||||
className={`w-full justify-center flex items-center gap-2 py-2 rounded-lg transition-colors font-medium ${
|
||||
resendStatus === 'sent'
|
||||
className={`w-full justify-center flex items-center gap-2 py-2 md:py-2.5 rounded-lg transition-colors font-medium text-sm md:text-base ${
|
||||
resendStatus === 'sent'
|
||||
? 'bg-green-500/20 text-green-400 cursor-default'
|
||||
: 'bg-slate-700 hover:bg-slate-600 text-white'
|
||||
}`}
|
||||
@@ -151,7 +152,7 @@ export const Generate: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={reset}
|
||||
className="btn-secondary w-full justify-center"
|
||||
className="btn-secondary w-full justify-center text-sm md:text-base"
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
Create Another
|
||||
|
||||
@@ -39,15 +39,16 @@ export const Home: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="flex justify-between items-end">
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<header className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Dashboard</h1>
|
||||
<p className="text-slate-400">Manage your events and attendees</p>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-2">Dashboard</h1>
|
||||
<p className="text-slate-400 text-sm md:text-base">Manage your events and attendees</p>
|
||||
</div>
|
||||
<Link to="/generate" className="btn-primary">
|
||||
<Link to="/generate" className="btn-primary whitespace-nowrap">
|
||||
<Plus size={20} />
|
||||
New Ticket
|
||||
<span className="hidden sm:inline">New Ticket</span>
|
||||
<span className="sm:hidden">New</span>
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
@@ -77,8 +78,8 @@ export const Home: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Recent Tickets</h2>
|
||||
<div className="glass-panel p-4 md:p-6">
|
||||
<h2 className="text-lg md:text-xl font-bold mb-4">Recent Tickets</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Loading tickets...
|
||||
@@ -92,36 +93,36 @@ export const Home: React.FC = () => {
|
||||
No tickets generated yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<div className="overflow-x-auto -mx-4 md:mx-0">
|
||||
<table className="w-full text-left border-collapse min-w-[600px]">
|
||||
<thead>
|
||||
<tr className="text-slate-400 border-b border-slate-700">
|
||||
<th className="p-4">Name</th>
|
||||
<th className="p-4">Email</th>
|
||||
<th className="p-4">Type</th>
|
||||
<th className="p-4">Status</th>
|
||||
<th className="p-4 text-right">Actions</th>
|
||||
<tr className="text-slate-400 border-b border-slate-700 text-sm">
|
||||
<th className="p-3 md:p-4">Name</th>
|
||||
<th className="p-3 md:p-4 hidden sm:table-cell">Email</th>
|
||||
<th className="p-3 md:p-4">Type</th>
|
||||
<th className="p-3 md:p-4">Status</th>
|
||||
<th className="p-3 md:p-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{tickets.slice().reverse().map(ticket => (
|
||||
<tr key={ticket.id} className="border-b border-slate-800 hover:bg-slate-800/50">
|
||||
<td className="p-4 font-medium">{ticket.attendeeName}</td>
|
||||
<td className="p-4 text-slate-400">{ticket.attendeeEmail}</td>
|
||||
<td className="p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${ticket.ticketType === 'Partyborner' ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
|
||||
<tr key={ticket.id} className="border-b border-slate-800 hover:bg-slate-800/50 text-sm">
|
||||
<td className="p-3 md:p-4 font-medium">{ticket.attendeeName}</td>
|
||||
<td className="p-3 md:p-4 text-slate-400 hidden sm:table-cell">{ticket.attendeeEmail}</td>
|
||||
<td className="p-3 md:p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${ticket.ticketType === 'Partyborner' ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{ticket.ticketType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${ticket.status === 'valid' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
|
||||
<td className="p-3 md:p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${ticket.status === 'valid' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
|
||||
}`}>
|
||||
{ticket.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<td className="p-3 md:p-4 text-right">
|
||||
<button
|
||||
onClick={() => handleDelete(ticket.id)}
|
||||
className="text-slate-500 hover:text-red-400 transition-colors p-2"
|
||||
|
||||
@@ -76,12 +76,12 @@ export const Scan: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Scan Tickets</h1>
|
||||
<p className="text-slate-400">Use your camera to verify attendee tickets.</p>
|
||||
<div className="text-center mb-6 md:mb-8">
|
||||
<h1 className="text-2xl md:text-3xl font-bold mb-2">Scan Tickets</h1>
|
||||
<p className="text-slate-400 text-sm md:text-base">Use your camera to verify attendee tickets.</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 overflow-hidden">
|
||||
<div className="glass-panel p-4 md:p-6 overflow-hidden">
|
||||
{!scanResult ? (
|
||||
<div className="relative">
|
||||
<div id="reader" className="w-full rounded-lg overflow-hidden"></div>
|
||||
@@ -90,32 +90,32 @@ export const Scan: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 animate-in fade-in zoom-in duration-300">
|
||||
<div className="text-center py-6 md:py-8 animate-in fade-in zoom-in duration-300">
|
||||
{scanResult.success ? (
|
||||
<div className="text-green-500 mb-4 flex justify-center">
|
||||
<CheckCircle size={64} />
|
||||
<CheckCircle size={48} className="md:w-16 md:h-16" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-500 mb-4 flex justify-center">
|
||||
<XCircle size={64} />
|
||||
<XCircle size={48} className="md:w-16 md:h-16" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="text-2xl font-bold mb-2">
|
||||
<h2 className="text-xl md:text-2xl font-bold mb-2">
|
||||
{scanResult.message}
|
||||
</h2>
|
||||
|
||||
{scanResult.ticket && (
|
||||
<div className="bg-slate-800/50 p-4 rounded-lg inline-block text-left mt-4 mb-6">
|
||||
<p className="text-slate-400 text-sm">Attendee</p>
|
||||
<p className="font-medium text-lg">{scanResult.ticket.attendeeName}</p>
|
||||
<p className="text-slate-400 text-sm mt-2">Type</p>
|
||||
<p className="font-medium text-lg">{scanResult.ticket.ticketType}</p>
|
||||
<div className="bg-slate-800/50 p-4 rounded-lg inline-block text-left mt-4 mb-6 w-full max-w-xs">
|
||||
<p className="text-slate-400 text-xs md:text-sm">Attendee</p>
|
||||
<p className="font-medium text-base md:text-lg">{scanResult.ticket.attendeeName}</p>
|
||||
<p className="text-slate-400 text-xs md:text-sm mt-2">Type</p>
|
||||
<p className="font-medium text-base md:text-lg">{scanResult.ticket.ticketType}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button onClick={resetScanner} className="btn-primary">
|
||||
<button onClick={resetScanner} className="btn-primary text-sm md:text-base">
|
||||
<RefreshCw size={18} />
|
||||
Scan Next
|
||||
</button>
|
||||
|
||||
@@ -26,8 +26,12 @@ export class TicketsService {
|
||||
});
|
||||
|
||||
try {
|
||||
// Generate QR code buffer from ticket ID
|
||||
const qrCodeBuffer = await QRCode.toBuffer(ticket.id, {
|
||||
// Generate QR code buffer with same format as frontend
|
||||
const qrCodeData = JSON.stringify({
|
||||
id: ticket.id,
|
||||
type: ticket.ticketType
|
||||
});
|
||||
const qrCodeBuffer = await QRCode.toBuffer(qrCodeData, {
|
||||
errorCorrectionLevel: 'H', // High error correction
|
||||
type: 'png', // Output as PNG image
|
||||
width: 250, // QR code width
|
||||
@@ -70,7 +74,12 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
try {
|
||||
const qrCodeBuffer = await QRCode.toBuffer(ticket.id, {
|
||||
// Generate QR code buffer with same format as frontend
|
||||
const qrCodeData = JSON.stringify({
|
||||
id: ticket.id,
|
||||
type: ticket.ticketType
|
||||
});
|
||||
const qrCodeBuffer = await QRCode.toBuffer(qrCodeData, {
|
||||
errorCorrectionLevel: 'H',
|
||||
type: 'png',
|
||||
width: 250,
|
||||
|
||||
Reference in New Issue
Block a user