import { useState, useRef, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import Layout from '../components/layout/Layout'; import { useAuth } from '../contexts/AuthContext'; import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode, Edit2, Filter, X } from 'lucide-react'; import { connectSocket, disconnectSocket, getSocket } from '../services/socket'; import { eventsAPI, heatsAPI } from '../services/api'; import HeatsBanner from '../components/heats/HeatsBanner'; const EventChatPage = () => { const { slug } = useParams(); const { user } = useAuth(); const navigate = useNavigate(); const [event, setEvent] = useState(null); const [isParticipant, setIsParticipant] = useState(false); const [loading, setLoading] = useState(true); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [activeUsers, setActiveUsers] = useState([]); const [checkedInUsers, setCheckedInUsers] = useState([]); 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); // Heats state const [myHeats, setMyHeats] = useState([]); const [userHeats, setUserHeats] = useState(new Map()); const [showHeatsBanner, setShowHeatsBanner] = useState(false); const [hideMyHeats, setHideMyHeats] = useState(false); const [showHeatsModal, setShowHeatsModal] = useState(false); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; // Fetch event data and check participation useEffect(() => { const fetchEvent = async () => { try { setLoading(true); // Get all events with participation info const allEvents = await eventsAPI.getAll(); const eventData = allEvents.find(e => e.slug === slug); if (eventData) { setEvent(eventData); setIsParticipant(eventData.isJoined); } else { setEvent(null); setIsParticipant(false); } } catch (err) { console.error('Error loading event:', err); setEvent(null); setIsParticipant(false); } finally { setLoading(false); } }; fetchEvent(); }, [slug]); // Load heats data and checked-in users useEffect(() => { if (!event || !isParticipant) return; const loadData = async () => { try { // Load my heats const myHeatsData = await heatsAPI.getMyHeats(slug); setMyHeats(myHeatsData); setShowHeatsBanner(myHeatsData.length === 0); // Load all users' heats const allHeatsData = await heatsAPI.getAllHeats(slug); const heatsMap = new Map(); allHeatsData.forEach((userHeat) => { heatsMap.set(userHeat.userId, userHeat.heats); }); setUserHeats(heatsMap); // Load all checked-in users (participants) const eventDetails = await eventsAPI.getDetails(slug); if (eventDetails.data && eventDetails.data.participants) { const participants = eventDetails.data.participants .map(p => ({ userId: p.userId, username: p.username, avatar: p.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${p.username}`, firstName: p.firstName, lastName: p.lastName, })) .filter(p => p.userId !== user.id); // Exclude current user setCheckedInUsers(participants); } } catch (error) { console.error('Failed to load data:', error); } }; loadData(); }, [event, isParticipant, slug, user.id]); useEffect(() => { scrollToBottom(); }, [messages]); useEffect(() => { if (!event) return; // Connect to Socket.IO const socket = connectSocket(); if (!socket) { console.error('Failed to connect to socket'); return; } // Socket event listeners socket.on('connect', () => { setIsConnected(true); // Join event room socket.emit('join_event_room', { slug }); }); socket.on('disconnect', () => { setIsConnected(false); }); // Receive message history (initial 20 messages) socket.on('message_history', (history) => { setMessages(history); setHasMore(history.length === 20); }); // Receive new messages socket.on('event_message', (message) => { setMessages((prev) => [...prev, message]); }); // Receive active users list socket.on('active_users', (users) => { // Filter out duplicates and current user const uniqueUsers = users .filter((u, index, self) => index === self.findIndex((t) => t.userId === u.userId) ) .filter((u) => u.userId !== user.id); setActiveUsers(uniqueUsers); }); // User joined notification socket.on('user_joined', (userData) => { console.log(`${userData.username} joined the room`); }); // User left notification socket.on('user_left', (userData) => { console.log(`${userData.username} left the room`); }); // Heats updated notification socket.on('heats_updated', (data) => { const { userId, heats } = data; // Update userHeats map setUserHeats((prev) => { const newMap = new Map(prev); if (heats && heats.length > 0) { newMap.set(userId, heats); } else { newMap.delete(userId); } return newMap; }); // If it's the current user, update myHeats if (userId === user.id) { setMyHeats(heats || []); setShowHeatsBanner(heats.length === 0); } }); // Cleanup return () => { socket.emit('leave_event_room'); socket.off('connect'); socket.off('disconnect'); socket.off('message_history'); socket.off('event_message'); socket.off('active_users'); socket.off('user_joined'); socket.off('user_left'); socket.off('heats_updated'); }; }, [event, slug, user.id]); const handleSendMessage = (e) => { e.preventDefault(); if (!newMessage.trim()) return; const socket = getSocket(); if (!socket || !socket.connected) { alert('Not connected to chat server'); return; } // Send message via Socket.IO socket.emit('send_event_message', { content: newMessage, }); setNewMessage(''); }; const loadOlderMessages = async () => { if (loadingOlder || !hasMore || messages.length === 0) return; setLoadingOlder(true); try { const oldestMessageId = messages[0].id; const response = await eventsAPI.getMessages(slug, oldestMessageId, 20); if (response.data.length > 0) { // Save current scroll position const container = messagesContainerRef.current; const oldScrollHeight = container.scrollHeight; const oldScrollTop = container.scrollTop; // Prepend older messages setMessages((prev) => [...response.data, ...prev]); setHasMore(response.hasMore); // Restore scroll position (adjust for new content) setTimeout(() => { const newScrollHeight = container.scrollHeight; container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight); }, 0); } else { setHasMore(false); } } catch (error) { console.error('Failed to load older messages:', error); } finally { setLoadingOlder(false); } }; const handleMatchWith = (userId) => { // TODO: Implement match request alert(`Match request sent to user!`); setTimeout(() => { navigate(`/matches/1/chat`); }, 1000); }; const handleHeatsSave = () => { setShowHeatsBanner(false); setShowHeatsModal(false); // Heats will be updated via Socket.IO heats_updated event }; const formatHeat = (heat) => { const parts = [ heat.competitionType?.abbreviation || '', heat.division?.abbreviation || '', heat.heatNumber, ]; if (heat.role) { parts.push(heat.role.charAt(0)); // L or F } return parts.join(' '); }; const shouldHideUser = (userId) => { if (!hideMyHeats || myHeats.length === 0) return false; const targetUserHeats = userHeats.get(userId); if (!targetUserHeats || targetUserHeats.length === 0) return false; // Hide if ANY of their heats match ANY of my heats (same division + competition_type + heat_number) return targetUserHeats.some((targetHeat) => myHeats.some( (myHeat) => myHeat.divisionId === targetHeat.divisionId && myHeat.competitionTypeId === targetHeat.competitionTypeId && myHeat.heatNumber === targetHeat.heatNumber ) ); }; // Combine checked-in users with online status const getAllDisplayUsers = () => { const activeUserIds = new Set(activeUsers.map(u => u.userId)); // Merge checked-in users with online status const allUsers = checkedInUsers.map(user => ({ ...user, isOnline: activeUserIds.has(user.userId), })); // Sort: online first, then offline return allUsers.sort((a, b) => { if (a.isOnline === b.isOnline) return 0; return a.isOnline ? -1 : 1; }); }; 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; if (!container) return; const handleScroll = () => { if (container.scrollTop < 100 && !loadingOlder && hasMore) { loadOlderMessages(); } }; container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); }, [loadingOlder, hasMore, messages]); if (loading) { return (

Loading event...

); } if (!event) { return (
Event not found
); } // Check if user is participant if (!isParticipant) { return (

Check-in Required

You need to check in at the event venue by scanning the QR code to access this chat.

{event.name}

{event.location}

Back to Events {import.meta.env.DEV && ( View QR Code (dev) )}
); } return (
{/* Header */}

{event.name}

{event.location}

{isConnected ? '● Connected' : '● Disconnected'} {/* My Heats Display */} {myHeats.length > 0 && (
Your heats:
{myHeats.map((heat, idx) => ( {formatHeat(heat)} ))}
)}
{myHeats.length > 0 && ( )}
{/* Heats Banner */} {showHeatsBanner && ( setShowHeatsBanner(false)} /> )}
{/* Participants Sidebar */}

Participants ({getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length})

{activeUsers.length} online

{/* Filter Checkbox */} {myHeats.length > 0 && ( )}
{getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length === 0 && (

No other participants

)}
{getAllDisplayUsers() .filter((displayUser) => !shouldHideUser(displayUser.userId)) .map((displayUser) => { const thisUserHeats = userHeats.get(displayUser.userId) || []; const hasHeats = thisUserHeats.length > 0; return (
{displayUser.username} {/* Online/Offline indicator */}

{displayUser.username}

{/* Heat Badges */} {hasHeats && (
{thisUserHeats.slice(0, 3).map((heat, idx) => ( {formatHeat(heat)} ))} {thisUserHeats.length > 3 && ( +{thisUserHeats.length - 3} )}
)}
); })}
{/* Chat Area */}
{/* Messages */}
{/* Loading older messages indicator */} {loadingOlder && (
)} {messages.length === 0 && (
No messages yet. Start the conversation!
)} {messages.map((message) => { const isOwnMessage = message.userId === user.id; return (
{message.username}
{message.username} {new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{message.content}
); })}
{/* Message Input */}
setNewMessage(e.target.value)} placeholder="Write a message..." disabled={!isConnected} className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50" />
{/* Leave Event Button */}
{/* Leave Confirmation Modal */} {showLeaveModal && (

Leave Event?

This action cannot be undone

Are you sure you want to leave {event.name}? You will need to scan the QR code again to rejoin.

)} {/* Edit Heats Modal */} {showHeatsModal && (

Edit Your Competition Heats

setShowHeatsModal(false)} existingHeats={myHeats} />
)}
); }; export default EventChatPage;