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:
@@ -12,6 +12,7 @@ import ChatInput from '../components/chat/ChatInput';
|
||||
import ConfirmationModal from '../components/modals/ConfirmationModal';
|
||||
import Modal from '../components/modals/Modal';
|
||||
import useEventChat from '../hooks/useEventChat';
|
||||
import ParticipantsSidebar from '../components/events/ParticipantsSidebar';
|
||||
|
||||
const EventChatPage = () => {
|
||||
const { slug } = useParams();
|
||||
@@ -191,18 +192,6 @@ const EventChatPage = () => {
|
||||
// Heats will be updated via Socket.IO heats_updated event
|
||||
};
|
||||
|
||||
const formatHeat = (heat) => {
|
||||
const parts = [
|
||||
heat.competitionType?.abbreviation || '',
|
||||
heat.division?.abbreviation || '',
|
||||
heat.heatNumber,
|
||||
];
|
||||
if (heat.role) {
|
||||
parts.push(heat.role.charAt(0)); // L or F
|
||||
}
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const shouldHideUser = (userId) => {
|
||||
if (!hideMyHeats || myHeats.length === 0) return false;
|
||||
|
||||
@@ -403,97 +392,15 @@ const EventChatPage = () => {
|
||||
)}
|
||||
|
||||
<div className="flex h-[calc(100vh-280px)]">
|
||||
{/* Participants Sidebar */}
|
||||
<div className="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">
|
||||
Participants ({getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length})
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
{activeUsers.length} online
|
||||
</p>
|
||||
|
||||
{/* Filter Checkbox */}
|
||||
{myHeats.length > 0 && (
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer hover:text-gray-900">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hideMyHeats}
|
||||
onChange={(e) => setHideMyHeats(e.target.checked)}
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<Filter className="w-3 h-3" />
|
||||
<span>Hide users from my heats</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{getAllDisplayUsers().filter(u => !shouldHideUser(u.userId)).length === 0 && (
|
||||
<p className="text-sm text-gray-500">No other participants</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{getAllDisplayUsers()
|
||||
.filter((displayUser) => !shouldHideUser(displayUser.userId))
|
||||
.map((displayUser) => {
|
||||
const thisUserHeats = userHeats.get(displayUser.userId) || [];
|
||||
const hasHeats = thisUserHeats.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={displayUser.userId}
|
||||
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">
|
||||
<Avatar
|
||||
src={displayUser.avatar}
|
||||
username={displayUser.username}
|
||||
size={32}
|
||||
status={displayUser.isOnline ? 'online' : 'offline'}
|
||||
title={displayUser.username}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm font-medium truncate ${
|
||||
displayUser.isOnline ? 'text-gray-900' : 'text-gray-500'
|
||||
}`}>
|
||||
{displayUser.username}
|
||||
</p>
|
||||
{/* Heat Badges */}
|
||||
{hasHeats && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{thisUserHeats.slice(0, 3).map((heat, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="text-xs px-1.5 py-0.5 bg-amber-100 text-amber-800 rounded font-mono"
|
||||
>
|
||||
{formatHeat(heat)}
|
||||
</span>
|
||||
))}
|
||||
{thisUserHeats.length > 3 && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-gray-200 text-gray-700 rounded font-mono">
|
||||
+{thisUserHeats.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleMatchWith(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'}
|
||||
>
|
||||
<UserPlus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ParticipantsSidebar
|
||||
users={getAllDisplayUsers().filter(u => !shouldHideUser(u.userId))}
|
||||
activeUsers={activeUsers}
|
||||
userHeats={userHeats}
|
||||
myHeats={myHeats}
|
||||
hideMyHeats={hideMyHeats}
|
||||
onHideMyHeatsChange={setHideMyHeats}
|
||||
onMatchWith={handleMatchWith}
|
||||
/>
|
||||
|
||||
{/* Chat Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
|
||||
Reference in New Issue
Block a user