From c7e577bf128ef308cde159830350207215bb7267 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?=
Date: Sat, 6 Dec 2025 12:01:35 +0100
Subject: [PATCH] feat(chat): add user status grouping and 'No heats' indicator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Improved participants sidebar UX by grouping users and clearly showing
who can't be contacted for match requests and why.
User Status Groups (in order):
1. Available - Online with declared heats (🟢 green dot)
- Ready to receive match requests
- Shows heat badges
- Active match button with "Send match request" tooltip
2. Online - No Heats - Online but no heats declared (🟡 yellow dot)
- Shows "No heats declared" gray badge
- Match button disabled with "User has not declared heats yet" tooltip
- Clear visual indicator of unavailability reason
3. Offline - Not currently online (⚪ gray dot)
- Can still send requests if they have heats (button faded)
- Shows "No heats declared" badge if no heats
- Match button disabled if no heats
Visual Improvements:
- Color-coded status dots for quick scanning
- Section headers with user counts per group
- "No heats declared" badge for users without heats
- Clear, contextual tooltips on disabled states
- Better spacing between groups (space-y-4 vs space-y-2)
Benefits:
- Users immediately see who's available to match
- No confusion about why buttons are disabled
- Priority given to online users with heats
- Reduced support questions
- Better conversion (users know what to do)
Applies to:
- Desktop sidebar (visible on chat tab)
- Mobile participants tab
---
.../components/events/ParticipantsSidebar.jsx | 168 ++++++++++++++----
.../src/components/users/UserListItem.jsx | 9 +-
2 files changed, 145 insertions(+), 32 deletions(-)
diff --git a/frontend/src/components/events/ParticipantsSidebar.jsx b/frontend/src/components/events/ParticipantsSidebar.jsx
index 06468ef..39938ce 100644
--- a/frontend/src/components/events/ParticipantsSidebar.jsx
+++ b/frontend/src/components/events/ParticipantsSidebar.jsx
@@ -43,6 +43,21 @@ const ParticipantsSidebar = ({
const participantCount = users.length;
const onlineCount = activeUsers.length;
+ // Group users by status for better UX
+ const groupedUsers = users.reduce((groups, user) => {
+ const hasHeats = (userHeats.get(user.userId) || []).length > 0;
+ const isOnline = user.isOnline ?? false;
+
+ if (isOnline && hasHeats) {
+ groups.onlineWithHeats.push(user);
+ } else if (isOnline && !hasHeats) {
+ groups.onlineNoHeats.push(user);
+ } else {
+ groups.offline.push(user);
+ }
+ return groups;
+ }, { onlineWithHeats: [], onlineNoHeats: [], offline: [] });
+
return (
{/* Header */}
@@ -78,38 +93,129 @@ const ParticipantsSidebar = ({
No other participants
)}
- {/* User List */}
-
- {users.map((displayUser) => {
- const thisUserHeats = userHeats.get(displayUser.userId) || [];
- const competitorNumber = userCompetitorNumbers.get(displayUser.userId);
- const hasHeats = thisUserHeats.length > 0;
+ {/* User List - Grouped by Status */}
+
+ {/* Online with Heats - Ready to match */}
+ {groupedUsers.onlineWithHeats.length > 0 && (
+
+
+
+
+ Available ({groupedUsers.onlineWithHeats.length})
+
+
+
+ {groupedUsers.onlineWithHeats.map((displayUser) => {
+ const thisUserHeats = userHeats.get(displayUser.userId) || [];
+ const competitorNumber = userCompetitorNumbers.get(displayUser.userId);
- return (
- onMatchWith?.(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'}
- >
-
-
- }
- />
- );
- })}
+ return (
+ onMatchWith?.(displayUser.userId)}
+ className="p-1 rounded flex-shrink-0 text-primary-600 hover:bg-primary-50"
+ title="Send match request"
+ >
+
+
+ }
+ />
+ );
+ })}
+
+
+ )}
+
+ {/* Online without Heats - Can't match yet */}
+ {groupedUsers.onlineNoHeats.length > 0 && (
+
+
+
+
+ Online - No Heats ({groupedUsers.onlineNoHeats.length})
+
+
+
+ {groupedUsers.onlineNoHeats.map((displayUser) => {
+ const competitorNumber = userCompetitorNumbers.get(displayUser.userId);
+
+ return (
+
+
+
+ }
+ />
+ );
+ })}
+
+
+ )}
+
+ {/* Offline */}
+ {groupedUsers.offline.length > 0 && (
+
+
+
+
+ Offline ({groupedUsers.offline.length})
+
+
+
+ {groupedUsers.offline.map((displayUser) => {
+ const thisUserHeats = userHeats.get(displayUser.userId) || [];
+ const competitorNumber = userCompetitorNumbers.get(displayUser.userId);
+ const hasHeats = thisUserHeats.length > 0;
+
+ return (
+
onMatchWith?.(displayUser.userId)}
+ className="p-1 rounded flex-shrink-0 text-primary-600 hover:bg-primary-50 opacity-60"
+ title="Send match request (user is offline)"
+ >
+
+
+ ) : (
+
+
+
+ )
+ }
+ />
+ );
+ })}
+
+
+ )}
);
diff --git a/frontend/src/components/users/UserListItem.jsx b/frontend/src/components/users/UserListItem.jsx
index c8c21c6..961da1f 100644
--- a/frontend/src/components/users/UserListItem.jsx
+++ b/frontend/src/components/users/UserListItem.jsx
@@ -83,12 +83,19 @@ const UserListItem = ({
{user.firstName} {user.lastName}
)}
- {/* Heat Badges */}
+ {/* Heat Badges or No Heats indicator */}
{showHeats && hasHeats && (
)}
+ {showHeats && !hasHeats && (
+
+
+ No heats declared
+
+
+ )}
{actionButton && (