AttachmentPreview.tsx (1823B)
1 interface AttachmentPreviewProps { 2 file: { name: string; type: string; size: number }; 3 onRemove: () => void; 4 thumbnailUrl?: string; 5 progress?: number; 6 error?: string; 7 } 8 9 function formatFileSize(bytes: number): string { 10 if (bytes < 1024) return `${bytes} B`; 11 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; 12 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; 13 } 14 15 function isImageFile(mimeType: string): boolean { 16 return mimeType.startsWith("image/"); 17 } 18 19 export function AttachmentPreview({ 20 file, 21 onRemove, 22 thumbnailUrl, 23 progress, 24 error, 25 }: AttachmentPreviewProps) { 26 return ( 27 <div className="flex items-center gap-2 rounded-lg bg-gray-800 px-3 py-2 text-sm"> 28 {isImageFile(file.type) && thumbnailUrl ? ( 29 <img 30 src={thumbnailUrl} 31 alt={file.name} 32 className="h-10 w-10 rounded object-cover" 33 /> 34 ) : ( 35 <span className="text-gray-400">📄</span> 36 )} 37 38 <div className="flex-1 min-w-0"> 39 <p className="truncate text-gray-200">{file.name}</p> 40 <p className="text-xs text-gray-500">{formatFileSize(file.size)}</p> 41 {progress !== undefined && ( 42 <div className="mt-1 h-1 w-full rounded bg-gray-700"> 43 <div 44 className="h-1 rounded bg-blue-500 transition-all" 45 style={{ width: `${Math.min(progress, 100)}%` }} 46 /> 47 </div> 48 )} 49 {error && <p className="text-xs text-red-400">{error}</p>} 50 </div> 51 52 <span className="text-xs text-gray-400">{progress}%</span> 53 54 <button 55 type="button" 56 onClick={onRemove} 57 className="ml-1 rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-300" 58 aria-label="Remove attachment" 59 > 60 ✕ 61 </button> 62 </div> 63 ); 64 }