feat(dashboard): add online count for events

Show real-time count of users currently in each event chat room.
- Backend: Export getEventsOnlineCounts from socket module
- Dashboard API: Include onlineCount for each active event
- Frontend: Display online count with animated green dot indicator
This commit is contained in:
Radosław Gierwiało
2025-11-21 21:41:16 +01:00
parent c15031db9f
commit 2c0620db6a
4 changed files with 88 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const { authenticate } = require('../middleware/auth'); const { authenticate } = require('../middleware/auth');
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const { getEventsOnlineCounts } = require('../socket');
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -38,6 +39,15 @@ router.get('/', authenticate, async (req, res, next) => {
}, },
}); });
// Get online counts for all events (safely handle if socket not initialized)
let onlineCounts = {};
try {
const eventIds = eventParticipants.map(ep => ep.event.id);
onlineCounts = getEventsOnlineCounts(eventIds);
} catch (_) {
// Socket may not be initialized (e.g., during tests)
}
// Get user's heats for each event // Get user's heats for each event
const activeEvents = await Promise.all( const activeEvents = await Promise.all(
eventParticipants.map(async (ep) => { eventParticipants.map(async (ep) => {
@@ -72,6 +82,7 @@ router.get('/', authenticate, async (req, res, next) => {
startDate: ep.event.startDate, startDate: ep.event.startDate,
endDate: ep.event.endDate, endDate: ep.event.endDate,
participantsCount: ep.event.participantsCount, participantsCount: ep.event.participantsCount,
onlineCount: onlineCounts[ep.event.id] || 0,
myHeats: heats.map((h) => ({ myHeats: heats.map((h) => ({
id: h.id, id: h.id,
competitionType: h.competitionType, competitionType: h.competitionType,

View File

@@ -461,4 +461,21 @@ function initializeSocket(httpServer) {
return io; return io;
} }
module.exports = { initializeSocket, getIO }; // Get count of online users for a specific event
function getEventOnlineCount(eventId) {
if (!activeUsers.has(eventId)) {
return 0;
}
return activeUsers.get(eventId).size;
}
// Get online counts for multiple events
function getEventsOnlineCounts(eventIds) {
const counts = {};
for (const eventId of eventIds) {
counts[eventId] = getEventOnlineCount(eventId);
}
return counts;
}
module.exports = { initializeSocket, getIO, getEventOnlineCount, getEventsOnlineCounts };

View File

@@ -326,6 +326,12 @@ const EventCard = ({ event }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="w-4 h-4 flex-shrink-0" /> <Users className="w-4 h-4 flex-shrink-0" />
<span>{event.participantsCount} participants</span> <span>{event.participantsCount} participants</span>
{event.onlineCount > 0 && (
<span className="text-green-600 flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
{event.onlineCount} online
</span>
)}
</div> </div>
</div> </div>

View File

@@ -128,6 +128,59 @@ describe('DashboardPage', () => {
}); });
}); });
it('should display online count when users are online', async () => {
dashboardAPI.getData.mockResolvedValue({
activeEvents: [
{
id: 1,
slug: 'test-event',
name: 'Test Event',
location: 'Warsaw',
startDate: '2025-12-01',
endDate: '2025-12-03',
participantsCount: 50,
onlineCount: 5,
myHeats: [],
},
],
activeMatches: [],
matchRequests: { incoming: [], outgoing: [] },
});
renderWithRouter(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText('5 online')).toBeInTheDocument();
});
});
it('should not display online count when zero', async () => {
dashboardAPI.getData.mockResolvedValue({
activeEvents: [
{
id: 1,
slug: 'test-event',
name: 'Test Event',
location: 'Warsaw',
startDate: '2025-12-01',
endDate: '2025-12-03',
participantsCount: 50,
onlineCount: 0,
myHeats: [],
},
],
activeMatches: [],
matchRequests: { incoming: [], outgoing: [] },
});
renderWithRouter(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText('Test Event')).toBeInTheDocument();
});
expect(screen.queryByText(/online/)).not.toBeInTheDocument();
});
it('should navigate to event chat when clicking Enter Chat', async () => { it('should navigate to event chat when clicking Enter Chat', async () => {
dashboardAPI.getData.mockResolvedValue({ dashboardAPI.getData.mockResolvedValue({
activeEvents: [ activeEvents: [