Files
spotlightcam/frontend/src/pages/EventChatPage.jsx

565 lines
20 KiB
React
Raw Normal View History

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';
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
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';
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)
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
useEffect(() => {
if (!event) return;
const socket = getSocket();
if (!socket) return;
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
// 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);
}
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
};
socket.on('heats_updated', handleHeatsUpdated);
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
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 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 (
<Layout>
<div className="max-w-4xl mx-auto flex items-center justify-center min-h-[400px]">
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-12 h-12 animate-spin text-primary-600" />
<p className="text-gray-600">Loading event...</p>
</div>
</div>
</Layout>
);
}
if (!event) {
return (
<Layout>
<div className="max-w-4xl mx-auto">
<div className="bg-red-50 border border-red-200 rounded-md p-4 text-red-700">
Event not found
</div>
</div>
</Layout>
);
}
// Check if user is participant
if (!isParticipant) {
return (
<Layout>
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-md p-8">
<div className="text-center">
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<QrCode className="w-8 h-8 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Check-in Required
</h2>
<p className="text-gray-600 mb-6">
You need to check in at the event venue by scanning the QR code to access this chat.
</p>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-sm text-amber-800 font-medium mb-1">
{event.name}
</p>
<p className="text-sm text-amber-700">
{event.location}
</p>
</div>
<div className="flex gap-3 justify-center">
<Link
to="/events"
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Back to Events
</Link>
{import.meta.env.DEV && (
<Link
to={`/events/${slug}/details`}
className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
View QR Code (dev)
</Link>
)}
</div>
</div>
</div>
</div>
</Layout>
);
}
return (
<Layout>
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow-md overflow-hidden">
{/* Header */}
<div className="bg-primary-600 text-white p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h2 className="text-2xl font-bold">{event.name}</h2>
<p className="text-primary-100 text-sm">{event.location}</p>
<div className="mt-2 flex items-center gap-3">
<span className={`text-xs px-2 py-1 rounded ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}>
{isConnected ? '● Connected' : '● Disconnected'}
</span>
{/* My Heats Display */}
{myHeats.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-primary-100">Your heats:</span>
<div className="flex flex-wrap gap-1">
{myHeats.map((heat, idx) => (
<span
key={idx}
className="text-xs px-2 py-0.5 bg-primary-700 text-white rounded font-mono"
>
{formatHeat(heat)}
</span>
))}
</div>
</div>
)}
</div>
</div>
{myHeats.length > 0 && (
<button
onClick={() => setShowHeatsModal(true)}
className="flex items-center gap-2 px-3 py-2 bg-primary-700 hover:bg-primary-800 rounded-md transition-colors text-sm"
>
<Edit2 className="w-4 h-4" />
Edit Heats
</button>
)}
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
</div>
</div>
{/* Heats Banner */}
{showHeatsBanner && (
<HeatsBanner
slug={slug}
onSave={handleHeatsSave}
onDismiss={() => setShowHeatsBanner(false)}
/>
)}
<div className="flex h-[calc(100vh-280px)]">
{/* Participants Sidebar */}
<div className="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
<div className="mb-4">
<h3 className="font-semibold text-gray-900 mb-2">
Participants ({getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length})
</h3>
<p className="text-xs text-gray-500 mb-3">
{activeUsers.length} online
</p>
{/* Filter Checkbox */}
{myHeats.length > 0 && (
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer hover:text-gray-900">
<input
type="checkbox"
checked={hideMyHeats}
onChange={(e) => setHideMyHeats(e.target.checked)}
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<Filter className="w-3 h-3" />
<span>Hide users from my heats</span>
</label>
)}
</div>
{getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length === 0 && (
<p className="text-sm text-gray-500">No other participants</p>
feat: implement real-time chat with Socket.IO Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
2025-11-12 22:42:15 +01:00
)}
<div className="space-y-2">
{getAllDisplayUsers()
.filter((displayUser) => !shouldHideUser(displayUser.userId))
.map((displayUser) => {
const thisUserHeats = userHeats.get(displayUser.userId) || [];
const hasHeats = thisUserHeats.length > 0;
return (
<div
key={displayUser.userId}
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
<Avatar
src={displayUser.avatar}
username={displayUser.username}
size={32}
status={displayUser.isOnline ? 'online' : 'offline'}
title={displayUser.username}
/>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium truncate ${
displayUser.isOnline ? 'text-gray-900' : 'text-gray-500'
}`}>
{displayUser.username}
</p>
{/* Heat Badges */}
{hasHeats && (
<div className="flex flex-wrap gap-1 mt-1">
{thisUserHeats.slice(0, 3).map((heat, idx) => (
<span
key={idx}
className="text-xs px-1.5 py-0.5 bg-amber-100 text-amber-800 rounded font-mono"
>
{formatHeat(heat)}
</span>
))}
{thisUserHeats.length > 3 && (
<span className="text-xs px-1.5 py-0.5 bg-gray-200 text-gray-700 rounded font-mono">
+{thisUserHeats.length - 3}
</span>
)}
</div>
)}
</div>
</div>
<button
onClick={() => handleMatchWith(displayUser.userId)}
disabled={!hasHeats}
className={`p-1 rounded flex-shrink-0 ${
hasHeats
? 'text-primary-600 hover:bg-primary-50'
: 'text-gray-300 cursor-not-allowed'
}`}
title={hasHeats ? 'Connect' : 'User has not declared heats'}
>
<UserPlus className="w-4 h-4" />
</button>
</div>
);
})}
</div>
</div>
{/* Chat Area */}
<div className="flex-1 flex flex-col">
<ChatMessageList
messages={messages}
currentUserId={user.id}
messagesEndRef={messagesEndRef}
messagesContainerRef={messagesContainerRef}
loadingOlder={loadingOlder}
hasMore={hasMore}
/>
{/* Message Input */}
<div className="border-t p-4">
<ChatInput
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onSubmit={handleSendMessage}
disabled={!isConnected}
placeholder="Write a message..."
/>
</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>
<ConfirmationModal
isOpen={showLeaveModal}
onClose={() => 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}
/>
<Modal
isOpen={showHeatsModal}
onClose={() => setShowHeatsModal(false)}
title="Edit Your Competition Heats"
>
<HeatsBanner
slug={slug}
onSave={handleHeatsSave}
onDismiss={() => setShowHeatsModal(false)}
existingHeats={myHeats}
/>
</Modal>
</div>
</Layout>
);
};
export default EventChatPage;