Add printable QR code feature for event check-in
This commit is contained in:
146
PRINTABLE_QR_CODE_FEATURE.md
Normal file
146
PRINTABLE_QR_CODE_FEATURE.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
202
src/pages/PrintableQRCodePage.tsx
Normal file
202
src/pages/PrintableQRCodePage.tsx
Normal 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;
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user