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:
99
frontend/src/components/webrtc/FileTransferProgress.jsx
Normal file
99
frontend/src/components/webrtc/FileTransferProgress.jsx
Normal 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;
|
||||
78
frontend/src/components/webrtc/LinkShareInput.jsx
Normal file
78
frontend/src/components/webrtc/LinkShareInput.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user