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 && (