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, matchesAPI } from '../services/api'; import HeatsBanner from '../components/heats/HeatsBanner'; import Avatar from '../components/common/Avatar'; import ChatMessageList from '../components/chat/ChatMessageList'; import ChatInput from '../components/chat/ChatInput'; import ConfirmationModal from '../components/modals/ConfirmationModal'; import Modal from '../components/modals/Modal'; import useEventChat from '../hooks/useEventChat'; import ParticipantsSidebar from '../components/events/ParticipantsSidebar'; 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 [checkedInUsers, setCheckedInUsers] = useState([]); const [showLeaveModal, setShowLeaveModal] = useState(false); const [isLeaving, setIsLeaving] = useState(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); // Event Chat hook - manages messages, Socket.IO, and active users const { messages, newMessage, setNewMessage, activeUsers, isConnected, loadingOlder, hasMore, sendMessage: handleSendMessage, loadOlderMessages } = useEventChat(slug, user?.id, event, messagesContainerRef); // 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]); // Heats updates listener (specific to EventChatPage) useEffect(() => { if (!event) return; const socket = getSocket(); if (!socket) return; // Heats updated notification const handleHeatsUpdated = (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); } }; socket.on('heats_updated', handleHeatsUpdated); return () => { socket.off('heats_updated', handleHeatsUpdated); }; }, [event, user.id]); const handleMatchWith = async (userId) => { try { const result = await matchesAPI.createMatch(userId, slug); // Show success message alert(`Match request sent successfully! The user will be notified.`); // Optional: Navigate to matches page or refresh matches list // For now, we just show a success message } catch (error) { console.error('Failed to send match request:', error); // Show appropriate error message if (error.status === 400 && error.message.includes('already exists')) { alert('You already have a match request with this user.'); } else if (error.status === 403) { alert('You must be a participant of this event to send match requests.'); } else if (error.status === 404) { alert('Event not found.'); } else { alert('Failed to send match request. Please try again.'); } } }; const handleHeatsSave = () => { setShowHeatsBanner(false); setShowHeatsModal(false); // Heats will be updated via Socket.IO heats_updated event }; 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)} /> )}
!shouldHideUser(u.userId))} activeUsers={activeUsers} userHeats={userHeats} myHeats={myHeats} hideMyHeats={hideMyHeats} onHideMyHeatsChange={setHideMyHeats} onMatchWith={handleMatchWith} /> {/* Chat Area */}
{/* Message Input */}
setNewMessage(e.target.value)} onSubmit={handleSendMessage} disabled={!isConnected} placeholder="Write a message..." />
{/* Leave Event Button */}
setShowLeaveModal(false)} onConfirm={handleLeaveEvent} title="Leave Event?" description="This action cannot be undone" message={`Are you sure you want to leave ${event?.name || 'this event'}? You will need to scan the QR code again to rejoin.`} confirmText="Leave Event" isLoading={isLeaving} loadingText="Leaving..." icon={AlertTriangle} /> setShowHeatsModal(false)} title="Edit Your Competition Heats" > setShowHeatsModal(false)} existingHeats={myHeats} />
); }; export default EventChatPage;