Add printable QR code feature for event check-in

This commit is contained in:
2025-11-20 19:30:59 +00:00
parent 4baa2892b9
commit 2dcd1ce78d
5 changed files with 379 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
# Printable QR Code Feature
## Overview
Users can now print their personal check-in QR code for easy access at cleanup events. This feature provides a dedicated printable page with a larger QR code and clear instructions.
## Implementation
### New Page
**`src/pages/PrintableQRCodePage.tsx`**
- Dedicated page for printing QR codes
- Larger QR code (300x300px) for better scanning
- Print-optimized layout with CSS media queries
- User information and instructions included
- Print button triggers browser print dialog
- Back button for easy navigation
### Modified Files
**`src/components/user/UserQRCode.tsx`**
- Added "Print QR Code" button
- Button navigates to the printable page
- Maintains existing QR code display functionality
**`src/App.tsx`**
- Added route: `/qr-code/print`
- Protected route (requires authentication)
- Lazy loaded for performance
**`src/pages/index.ts`**
- Added export for `PrintableQRCodePage`
## Features
### Printable Page Layout
1. **Header Section**
- Event check-in title
- Litter Scouts branding
2. **QR Code Display**
- Large 300x300px QR code
- High error correction level (H)
- Border for visual separation
3. **User Information**
- User avatar (if available)
- Full name or username
- Username handle
4. **Instructions**
- Step-by-step guide on how to use the QR code
- Numbered list for clarity
5. **Footer**
- Security reminder
- Unique code notice
### Print Optimization
- CSS media queries hide navigation elements when printing
- Only the QR code content is visible in print
- Optimized layout for standard paper sizes
- Clean, professional appearance
### User Experience
1. User views their QR code on profile page
2. Clicks "Print QR Code" button
3. Navigates to printable page
4. Reviews QR code and information
5. Clicks "Print QR Code" button or uses browser print (Cmd/Ctrl+P)
6. Prints or saves as PDF
7. Can navigate back using "Back" button
## Benefits
- **Convenience**: Users can print QR codes in advance
- **Reliability**: No need for phone battery or internet at events
- **Accessibility**: Physical QR codes work for all users
- **Professional**: Clean, branded printout
- **Flexible**: Can save as PDF or print on paper
## Technical Details
### QR Code Data Format
```json
{
"userId": "user_clerk_id",
"type": "attendee-checkin",
"timestamp": 1234567890
}
```
### Print Styles
```css
@media print {
/* Hide everything except printable content */
body * {
visibility: hidden;
}
#printable-qr-code, #printable-qr-code * {
visibility: visible;
}
/* Position printable content at top */
#printable-qr-code {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
/* Hide action buttons */
.no-print {
display: none !important;
}
}
```
## Usage
### For Users
1. Navigate to your profile page
2. Scroll to "Your Check-in QR Code" section
3. Click "Print QR Code" button
4. Review the printable page
5. Click "Print QR Code" or use Cmd/Ctrl+P
6. Print or save as PDF
### For Event Organizers
- Attendees can show printed QR codes
- Scan works the same as digital QR codes
- No special handling required
- Reduces check-in time at events
## Future Enhancements
- Add event-specific QR codes
- Include event details on printout
- Support for multiple QR codes per page
- Customizable branding options
- QR code expiration dates

View File

@@ -25,6 +25,7 @@ const WhyCleanPage = lazy(() => import('./pages/WhyCleanPage'));
const UserProfilePage = lazy(() => import('./pages/UserProfilePage')); const UserProfilePage = lazy(() => import('./pages/UserProfilePage'));
const EnvironmentalDataDemo = lazy(() => import('./pages/EnvironmentalDataDemo')); const EnvironmentalDataDemo = lazy(() => import('./pages/EnvironmentalDataDemo'));
const EventCheckInPage = lazy(() => import('./pages/EventCheckInPage')); const EventCheckInPage = lazy(() => import('./pages/EventCheckInPage'));
const PrintableQRCodePage = lazy(() => import('./pages/PrintableQRCodePage'));
// Services // Services
import { apiClient } from './services/api'; import { apiClient } from './services/api';
@@ -163,6 +164,14 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/qr-code/print"
element={
<ProtectedRoute>
<PrintableQRCodePage />
</ProtectedRoute>
}
/>
{/* Demo routes for testing */} {/* Demo routes for testing */}
<Route <Route

View File

@@ -1,8 +1,10 @@
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { useUser } from '@clerk/clerk-react'; import { useUser } from '@clerk/clerk-react';
import { useNavigate } from 'react-router-dom';
export function UserQRCode() { export function UserQRCode() {
const { user } = useUser(); const { user } = useUser();
const navigate = useNavigate();
if (!user) return null; if (!user) return null;
@@ -32,6 +34,25 @@ export function UserQRCode() {
<p className="text-xs text-gray-500 mt-4 text-center"> <p className="text-xs text-gray-500 mt-4 text-center">
{user.username || user.firstName || 'User'} {user.username || user.firstName || 'User'}
</p> </p>
<button
onClick={() => navigate('/qr-code/print')}
className="w-full mt-4 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium inline-flex items-center justify-center"
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
/>
</svg>
Print QR Code
</button>
</div> </div>
); );
} }

View File

@@ -0,0 +1,202 @@
import { useEffect } from 'react';
import { useUser } from '@clerk/clerk-react';
import { QRCodeSVG } from 'qrcode.react';
import { useNavigate } from 'react-router-dom';
export function PrintableQRCodePage() {
const { user, isLoaded } = useUser();
const navigate = useNavigate();
useEffect(() => {
// Add print-specific styles
const style = document.createElement('style');
style.textContent = `
@media print {
body * {
visibility: hidden;
}
#printable-qr-code, #printable-qr-code * {
visibility: visible;
}
#printable-qr-code {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.no-print {
display: none !important;
}
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
if (!isLoaded) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
if (!user) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-md p-6 max-w-md w-full text-center">
<p className="text-gray-600 mb-4">Please sign in to view your QR code</p>
<button
onClick={() => navigate('/')}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
Go Home
</button>
</div>
</div>
);
}
const qrData = JSON.stringify({
userId: user.id,
type: 'attendee-checkin',
timestamp: Date.now(),
});
const handlePrint = () => {
window.print();
};
return (
<div className="min-h-screen bg-gray-50 py-8 px-4">
{/* Action Buttons - Hidden when printing */}
<div className="max-w-4xl mx-auto mb-6 no-print">
<div className="flex items-center justify-between">
<button
onClick={() => navigate(-1)}
className="text-blue-600 hover:text-blue-700 inline-flex items-center"
>
<svg
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</button>
<button
onClick={handlePrint}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors font-medium inline-flex items-center"
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
/>
</svg>
Print QR Code
</button>
</div>
</div>
{/* Printable Content */}
<div id="printable-qr-code" className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-md p-12">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Event Check-In QR Code
</h1>
<p className="text-gray-600">
Litter Scouts - Beach Cleanup Events
</p>
</div>
{/* QR Code */}
<div className="flex justify-center mb-8">
<div className="p-8 bg-white border-4 border-gray-200 rounded-lg">
<QRCodeSVG
value={qrData}
size={300}
level="H"
includeMargin={true}
/>
</div>
</div>
{/* User Info */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center mb-4">
{user.imageUrl && (
<img
src={user.imageUrl}
alt={user.fullName || user.username || 'User'}
className="w-16 h-16 rounded-full border-2 border-gray-300"
/>
)}
</div>
<h2 className="text-2xl font-semibold text-gray-900 mb-1">
{user.fullName || user.username || 'User'}
</h2>
<p className="text-gray-600">@{user.username}</p>
</div>
{/* Instructions */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-center">
How to Use
</h3>
<div className="space-y-2 text-gray-700">
<div className="flex items-start">
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm mr-3 flex-shrink-0 mt-0.5">
1
</span>
<p>Bring this QR code to any Litter Scouts cleanup event</p>
</div>
<div className="flex items-start">
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm mr-3 flex-shrink-0 mt-0.5">
2
</span>
<p>Show it to the event organizer for quick check-in</p>
</div>
<div className="flex items-start">
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm mr-3 flex-shrink-0 mt-0.5">
3
</span>
<p>Your attendance will be automatically recorded</p>
</div>
</div>
</div>
{/* Footer */}
<div className="mt-8 pt-6 border-t border-gray-200 text-center text-sm text-gray-500">
<p>This QR code is unique to your account</p>
<p className="mt-1">Keep it safe and don't share with others</p>
</div>
</div>
</div>
</div>
);
}
export default PrintableQRCodePage;

View File

@@ -9,3 +9,4 @@ export { ReportDetailDemo } from './ReportDetailDemo';
export { WhyCleanPage } from './WhyCleanPage'; export { WhyCleanPage } from './WhyCleanPage';
export { UserProfilePage } from './UserProfilePage'; export { UserProfilePage } from './UserProfilePage';
export { EnvironmentalDataDemo } from './EnvironmentalDataDemo'; export { EnvironmentalDataDemo } from './EnvironmentalDataDemo';
export { PrintableQRCodePage } from './PrintableQRCodePage';