feat: show all checked-in participants in event chat sidebar

- Display all event participants (not just online users)
- Add online/offline status indicator (green/gray dot)
- Sort users: online first, then offline
- Show participant count and online count separately
- Load participants via /api/events/:slug/details endpoint
- Users can see who's checked in and has declared heats even when offline

This allows users to see the full picture of event participation,
not just who's currently connected to the chat.
This commit is contained in:
Radosław Gierwiało
2025-11-14 18:04:10 +01:00
parent 92315d5a8c
commit e08492236a

View File

@@ -17,6 +17,7 @@ const EventChatPage = () => {
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);
@@ -65,11 +66,11 @@ const EventChatPage = () => {
fetchEvent();
}, [slug]);
// Load heats data
// Load heats data and checked-in users
useEffect(() => {
if (!event || !isParticipant) return;
const loadHeats = async () => {
const loadData = async () => {
try {
// Load my heats
const myHeatsData = await heatsAPI.getMyHeats(slug);
@@ -83,13 +84,28 @@ const EventChatPage = () => {
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.user.id,
username: p.user.username,
avatar: p.user.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${p.user.username}`,
firstName: p.user.firstName,
lastName: p.user.lastName,
}))
.filter(p => p.userId !== user.id); // Exclude current user
setCheckedInUsers(participants);
}
} catch (error) {
console.error('Failed to load heats:', error);
console.error('Failed to load data:', error);
}
};
loadHeats();
}, [event, isParticipant, slug]);
loadData();
}, [event, isParticipant, slug, user.id]);
useEffect(() => {
scrollToBottom();
@@ -279,6 +295,23 @@ const EventChatPage = () => {
);
};
// 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);
@@ -428,12 +461,15 @@ const EventChatPage = () => {
)}
<div className="flex h-[calc(100vh-280px)]">
{/* Active Users Sidebar */}
{/* 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-3">
Active users ({activeUsers.filter(u => !shouldHideUser(u.userId)).length})
<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 && (
@@ -450,30 +486,41 @@ const EventChatPage = () => {
)}
</div>
{activeUsers.filter(u => !shouldHideUser(u.userId)).length === 0 && (
<p className="text-sm text-gray-500">No other users online</p>
{getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length === 0 && (
<p className="text-sm text-gray-500">No other participants</p>
)}
<div className="space-y-2">
{activeUsers
.filter((activeUser) => !shouldHideUser(activeUser.userId))
.map((activeUser) => {
const thisUserHeats = userHeats.get(activeUser.userId) || [];
{getAllDisplayUsers()
.filter((displayUser) => !shouldHideUser(displayUser.userId))
.map((displayUser) => {
const thisUserHeats = userHeats.get(displayUser.userId) || [];
const hasHeats = thisUserHeats.length > 0;
return (
<div
key={activeUser.userId}
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">
<div className="relative flex-shrink-0">
<img
src={activeUser.avatar}
alt={activeUser.username}
className="w-8 h-8 rounded-full flex-shrink-0"
src={displayUser.avatar}
alt={displayUser.username}
className="w-8 h-8 rounded-full"
/>
{/* Online/Offline indicator */}
<div
className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-gray-50 ${
displayUser.isOnline ? 'bg-green-500' : 'bg-gray-400'
}`}
title={displayUser.isOnline ? 'Online' : 'Offline'}
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{activeUser.username}
<p className={`text-sm font-medium truncate ${
displayUser.isOnline ? 'text-gray-900' : 'text-gray-500'
}`}>
{displayUser.username}
</p>
{/* Heat Badges */}
{hasHeats && (
@@ -496,7 +543,7 @@ const EventChatPage = () => {
</div>
</div>
<button
onClick={() => handleMatchWith(activeUser.userId)}
onClick={() => handleMatchWith(displayUser.userId)}
disabled={!hasHeats}
className={`p-1 rounded flex-shrink-0 ${
hasHeats