refactor(frontend): Phase 3 - create advanced composite components
Extract complex UI sections into reusable composite components New Components Created: 1. HeatBadges (heats/HeatBadges.jsx) - Displays competition heats with compact notation - Configurable max visible badges with "+X more" overflow - Tooltips with full heat information 2. UserListItem (users/UserListItem.jsx) - Reusable user entry with avatar, username, full name - Optional heat badges display - Flexible action button slot (render props pattern) - Online/offline status support 3. ParticipantsSidebar (events/ParticipantsSidebar.jsx) - Complete sidebar component for event participants - Filter checkbox for hiding users from own heats - Participant and online counters - Integrated UserListItem with match actions 4. FileTransferProgress (webrtc/FileTransferProgress.jsx) - WebRTC P2P file transfer UI - Progress bar with percentage - Send/Cancel actions 5. LinkShareInput (webrtc/LinkShareInput.jsx) - Fallback link sharing when WebRTC unavailable - Google Drive, Dropbox link support Pages Refactored: - EventChatPage: 564 → 471 lines (-93 lines, -16%) * Replaced 90-line participants sidebar with <ParticipantsSidebar /> * Removed duplicate formatHeat logic (now in HeatBadges) - MatchChatPage: 446 → 369 lines (-77 lines, -17%) * Replaced 56-line file transfer UI with <FileTransferProgress /> * Replaced 39-line link input form with <LinkShareInput /> Phase 3 Total: -170 lines Grand Total (Phase 1+2+3): -559 lines (-17%) Final Results: - EventChatPage: 761 → 471 lines (-290 lines, -38% reduction) - MatchChatPage: 567 → 369 lines (-198 lines, -35% reduction) Benefits: - Massive complexity reduction in largest components - Composite components can be reused across pages - Better testability - each component tested independently - Cleaner code organization - single responsibility principle - Easier maintenance - changes in one place propagate everywhere
This commit is contained in:
93
frontend/src/components/users/UserListItem.jsx
Normal file
93
frontend/src/components/users/UserListItem.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import Avatar from '../common/Avatar';
|
||||
import HeatBadges from '../heats/HeatBadges';
|
||||
|
||||
/**
|
||||
* Reusable User List Item component
|
||||
* Displays user avatar, username, and optional heats with action button
|
||||
*
|
||||
* @param {object} user - User object with { userId, username, avatar, isOnline, firstName, lastName }
|
||||
* @param {Array} heats - Array of heat objects for this user
|
||||
* @param {boolean} showHeats - Whether to display heat badges (default: true)
|
||||
* @param {React.ReactNode} actionButton - Optional action button/element to display on right
|
||||
* @param {function} onClick - Optional click handler for the entire item
|
||||
* @param {string} className - Additional CSS classes
|
||||
* @param {boolean} linkToProfile - Whether username should link to profile (default: false)
|
||||
*
|
||||
* @example
|
||||
* <UserListItem
|
||||
* user={displayUser}
|
||||
* heats={userHeats.get(userId)}
|
||||
* actionButton={
|
||||
* <button onClick={() => handleMatch(userId)}>
|
||||
* <UserPlus />
|
||||
* </button>
|
||||
* }
|
||||
* />
|
||||
*/
|
||||
const UserListItem = ({
|
||||
user,
|
||||
heats = [],
|
||||
showHeats = true,
|
||||
actionButton,
|
||||
onClick,
|
||||
className = '',
|
||||
linkToProfile = false
|
||||
}) => {
|
||||
const hasHeats = heats && heats.length > 0;
|
||||
const isOnline = user?.isOnline ?? false;
|
||||
|
||||
const usernameClasses = `text-sm font-medium truncate ${
|
||||
isOnline ? 'text-gray-900' : 'text-gray-500'
|
||||
}`;
|
||||
|
||||
const usernameContent = linkToProfile ? (
|
||||
<Link
|
||||
to={`/profile/${user.username}`}
|
||||
className={`${usernameClasses} hover:text-primary-600`}
|
||||
>
|
||||
{user.username}
|
||||
</Link>
|
||||
) : (
|
||||
<p className={usernameClasses}>{user.username}</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<Avatar
|
||||
src={user.avatar}
|
||||
username={user.username}
|
||||
size={32}
|
||||
status={isOnline ? 'online' : 'offline'}
|
||||
title={user.username}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
{usernameContent}
|
||||
{/* Full name (optional) */}
|
||||
{(user.firstName || user.lastName) && (
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{user.firstName} {user.lastName}
|
||||
</p>
|
||||
)}
|
||||
{/* Heat Badges */}
|
||||
{showHeats && hasHeats && (
|
||||
<div className="mt-1">
|
||||
<HeatBadges heats={heats} maxVisible={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actionButton && (
|
||||
<div className="flex-shrink-0 ml-2">
|
||||
{actionButton}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserListItem;
|
||||
Reference in New Issue
Block a user