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:
@@ -17,6 +17,7 @@ const EventChatPage = () => {
|
|||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [activeUsers, setActiveUsers] = useState([]);
|
const [activeUsers, setActiveUsers] = useState([]);
|
||||||
|
const [checkedInUsers, setCheckedInUsers] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
@@ -65,11 +66,11 @@ const EventChatPage = () => {
|
|||||||
fetchEvent();
|
fetchEvent();
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
// Load heats data
|
// Load heats data and checked-in users
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!event || !isParticipant) return;
|
if (!event || !isParticipant) return;
|
||||||
|
|
||||||
const loadHeats = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
// Load my heats
|
// Load my heats
|
||||||
const myHeatsData = await heatsAPI.getMyHeats(slug);
|
const myHeatsData = await heatsAPI.getMyHeats(slug);
|
||||||
@@ -83,13 +84,28 @@ const EventChatPage = () => {
|
|||||||
heatsMap.set(userHeat.userId, userHeat.heats);
|
heatsMap.set(userHeat.userId, userHeat.heats);
|
||||||
});
|
});
|
||||||
setUserHeats(heatsMap);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load heats:', error);
|
console.error('Failed to load data:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadHeats();
|
loadData();
|
||||||
}, [event, isParticipant, slug]);
|
}, [event, isParticipant, slug, user.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
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 () => {
|
const handleLeaveEvent = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLeaving(true);
|
setIsLeaving(true);
|
||||||
@@ -428,12 +461,15 @@ const EventChatPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex h-[calc(100vh-280px)]">
|
<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="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="font-semibold text-gray-900 mb-3">
|
<h3 className="font-semibold text-gray-900 mb-2">
|
||||||
Active users ({activeUsers.filter(u => !shouldHideUser(u.userId)).length})
|
Participants ({getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length})
|
||||||
</h3>
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
|
{activeUsers.length} online
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Filter Checkbox */}
|
{/* Filter Checkbox */}
|
||||||
{myHeats.length > 0 && (
|
{myHeats.length > 0 && (
|
||||||
@@ -450,30 +486,41 @@ const EventChatPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeUsers.filter(u => !shouldHideUser(u.userId)).length === 0 && (
|
{getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length === 0 && (
|
||||||
<p className="text-sm text-gray-500">No other users online</p>
|
<p className="text-sm text-gray-500">No other participants</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{activeUsers
|
{getAllDisplayUsers()
|
||||||
.filter((activeUser) => !shouldHideUser(activeUser.userId))
|
.filter((displayUser) => !shouldHideUser(displayUser.userId))
|
||||||
.map((activeUser) => {
|
.map((displayUser) => {
|
||||||
const thisUserHeats = userHeats.get(activeUser.userId) || [];
|
const thisUserHeats = userHeats.get(displayUser.userId) || [];
|
||||||
const hasHeats = thisUserHeats.length > 0;
|
const hasHeats = thisUserHeats.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={activeUser.userId}
|
key={displayUser.userId}
|
||||||
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
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="flex items-center space-x-2 flex-1 min-w-0">
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={activeUser.avatar}
|
src={displayUser.avatar}
|
||||||
alt={activeUser.username}
|
alt={displayUser.username}
|
||||||
className="w-8 h-8 rounded-full flex-shrink-0"
|
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">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 truncate">
|
<p className={`text-sm font-medium truncate ${
|
||||||
{activeUser.username}
|
displayUser.isOnline ? 'text-gray-900' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{displayUser.username}
|
||||||
</p>
|
</p>
|
||||||
{/* Heat Badges */}
|
{/* Heat Badges */}
|
||||||
{hasHeats && (
|
{hasHeats && (
|
||||||
@@ -496,7 +543,7 @@ const EventChatPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMatchWith(activeUser.userId)}
|
onClick={() => handleMatchWith(displayUser.userId)}
|
||||||
disabled={!hasHeats}
|
disabled={!hasHeats}
|
||||||
className={`p-1 rounded flex-shrink-0 ${
|
className={`p-1 rounded flex-shrink-0 ${
|
||||||
hasHeats
|
hasHeats
|
||||||
|
|||||||
Reference in New Issue
Block a user