feat: add QR code event check-in system
Backend: - Add event_checkin_tokens table with unique tokens per event - Implement GET /api/events/:slug/details endpoint (on-demand token generation) - Implement POST /api/events/checkin/:token endpoint (date validation only in production) - Implement DELETE /api/events/:slug/leave endpoint - Add comprehensive test suite for check-in endpoints Frontend: - Add EventDetailsPage with QR code display, participant list, and stats - Add EventCheckinPage with success/error screens - Add "Leave Event" button with confirmation modal to EventChatPage - Install qrcode.react library for QR code generation - Update routing and API client with new endpoints Features: - QR codes valid from (startDate-1d) to (endDate+1d) - Development mode bypasses date validation for testing - Automatic participant count tracking - Duplicate check-in prevention - Token reuse for same event (generated once, cached)
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Send, UserPlus, Loader2 } from 'lucide-react';
|
||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle } from 'lucide-react';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||
import { eventsAPI } from '../services/api';
|
||||
|
||||
@@ -18,6 +18,8 @@ const EventChatPage = () => {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [showLeaveModal, setShowLeaveModal] = useState(false);
|
||||
const [isLeaving, setIsLeaving] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
const messagesContainerRef = useRef(null);
|
||||
|
||||
@@ -173,6 +175,28 @@ const EventChatPage = () => {
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleLeaveEvent = async () => {
|
||||
try {
|
||||
setIsLeaving(true);
|
||||
await eventsAPI.leave(slug);
|
||||
|
||||
// Disconnect socket
|
||||
const socket = getSocket();
|
||||
if (socket) {
|
||||
socket.emit('leave_event_room');
|
||||
}
|
||||
|
||||
// Redirect to events page
|
||||
navigate('/events');
|
||||
} catch (error) {
|
||||
console.error('Failed to leave event:', error);
|
||||
alert('Failed to leave event. Please try again.');
|
||||
} finally {
|
||||
setIsLeaving(false);
|
||||
setShowLeaveModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Infinite scroll - detect scroll to top
|
||||
useEffect(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
@@ -344,7 +368,67 @@ const EventChatPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leave Event Button */}
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
onClick={() => setShowLeaveModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Leave Event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leave Confirmation Modal */}
|
||||
{showLeaveModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center">
|
||||
<AlertTriangle className="text-red-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Leave Event?</h3>
|
||||
<p className="text-sm text-gray-600">This action cannot be undone</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 mb-6">
|
||||
Are you sure you want to leave <strong>{event.name}</strong>?
|
||||
You will need to scan the QR code again to rejoin.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowLeaveModal(false)}
|
||||
disabled={isLeaving}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-medium disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLeaveEvent}
|
||||
disabled={isLeaving}
|
||||
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLeaving ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
Leaving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogOut size={16} />
|
||||
Leave Event
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user