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:
Radosław Gierwiało
2025-11-21 17:10:53 +01:00
parent 9e74343c3b
commit 082105c5bf
7 changed files with 471 additions and 200 deletions

View File

@@ -0,0 +1,99 @@
import { Video, Upload, X } from 'lucide-react';
/**
* File Transfer Progress component for WebRTC P2P file sharing
* Displays selected file info, progress bar, and action buttons
*
* @param {File} file - Selected file object
* @param {number} progress - Transfer progress percentage (0-100)
* @param {boolean} isTransferring - Whether file is currently being transferred
* @param {function} onCancel - Cancel transfer handler
* @param {function} onSend - Start transfer handler
* @param {function} onRemoveFile - Remove selected file handler (X button)
*
* @example
* <FileTransferProgress
* file={selectedFile}
* progress={transferProgress}
* isTransferring={isTransferring}
* onCancel={handleCancelTransfer}
* onSend={handleStartTransfer}
* onRemoveFile={() => setSelectedFile(null)}
* />
*/
const FileTransferProgress = ({
file,
progress = 0,
isTransferring = false,
onCancel,
onSend,
onRemoveFile
}) => {
if (!file) return null;
const fileSizeMB = file ? (file.size / 1024 / 1024).toFixed(2) : '0.00';
return (
<div className="border-t bg-blue-50 p-4">
<div className="bg-white rounded-lg p-4 shadow-sm">
{/* File Info Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<Video className="w-6 h-6 text-primary-600 flex-shrink-0" />
<div className="min-w-0">
<p className="font-medium text-gray-900 truncate">{file.name}</p>
<p className="text-sm text-gray-500">{fileSizeMB} MB</p>
</div>
</div>
{!isTransferring && onRemoveFile && (
<button
onClick={onRemoveFile}
className="text-gray-400 hover:text-gray-600 transition-colors flex-shrink-0"
aria-label="Remove file"
>
<X className="w-5 h-5" />
</button>
)}
</div>
{/* Progress Bar & Actions */}
{isTransferring ? (
<>
<div className="mb-2">
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>Transferring via WebRTC...</span>
<span>{progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full transition-all duration-200"
style={{ width: `${progress}%` }}
role="progressbar"
aria-valuenow={progress}
aria-valuemin="0"
aria-valuemax="100"
/>
</div>
</div>
<button
onClick={onCancel}
className="w-full px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors font-medium"
>
Cancel
</button>
</>
) : (
<button
onClick={onSend}
className="w-full px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center justify-center space-x-2 font-medium"
>
<Upload className="w-4 h-4" />
<span>Send video (P2P)</span>
</button>
)}
</div>
</div>
);
};
export default FileTransferProgress;

View File

@@ -0,0 +1,78 @@
/**
* Link Share Input component
* Fallback for WebRTC - allows sharing video links (Google Drive, Dropbox, etc.)
*
* @param {boolean} isOpen - Whether the input form is visible
* @param {string} link - Current link value
* @param {function} onChange - Link input change handler
* @param {function} onSubmit - Form submit handler
* @param {function} onCancel - Cancel/close handler
*
* @example
* <LinkShareInput
* isOpen={showLinkInput}
* link={videoLink}
* onChange={(e) => setVideoLink(e.target.value)}
* onSubmit={handleSendLink}
* onCancel={() => {
* setShowLinkInput(false);
* setVideoLink('');
* }}
* />
*/
const LinkShareInput = ({
isOpen,
link,
onChange,
onSubmit,
onCancel
}) => {
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (link.trim()) {
onSubmit?.(e);
}
};
return (
<div className="border-t bg-yellow-50 p-4">
<div className="bg-white rounded-lg p-4 shadow-sm">
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Video link (Google Drive, Dropbox, etc.)
</label>
<input
type="url"
value={link}
onChange={onChange}
placeholder="https://drive.google.com/..."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
required
autoFocus
/>
</div>
<div className="flex space-x-2">
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors font-medium"
>
Send link
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors font-medium"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
export default LinkShareInput;