qr code - responsive

This commit is contained in:
DerJesen
2025-11-29 13:50:40 +01:00
parent f0b2e19494
commit 4d1291766b
6 changed files with 151 additions and 77 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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,