1+ import { useState , useRef } from 'react' ;
2+ import { Upload , X , File , Download , Loader2 } from 'lucide-react' ;
3+ import { Button } from '@/components/ui/button' ;
4+ import { Progress } from '@/components/ui/progress' ;
5+ import type { FileAttachment } from '@/types/note' ;
6+
7+ interface FileUploadProps {
8+ attachments ?: FileAttachment [ ] ;
9+ onUpload : ( files : FileList ) => Promise < void > ;
10+ onRemove : ( attachmentId : string ) => Promise < void > ;
11+ onDownload : ( attachment : FileAttachment ) => Promise < void > ;
12+ isUploading ?: boolean ;
13+ uploadProgress ?: number ;
14+ deletingIds ?: string [ ] ;
15+ }
16+
17+ export default function FileUpload ( {
18+ attachments = [ ] ,
19+ onUpload,
20+ onRemove,
21+ onDownload,
22+ isUploading = false ,
23+ uploadProgress = 0 ,
24+ deletingIds = [ ] ,
25+ } : FileUploadProps ) {
26+ const [ isDragOver , setIsDragOver ] = useState ( false ) ;
27+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
28+
29+ const handleDragOver = ( e : React . DragEvent ) => {
30+ e . preventDefault ( ) ;
31+ setIsDragOver ( true ) ;
32+ } ;
33+
34+ const handleDragLeave = ( e : React . DragEvent ) => {
35+ e . preventDefault ( ) ;
36+ setIsDragOver ( false ) ;
37+ } ;
38+
39+ const handleDrop = ( e : React . DragEvent ) => {
40+ e . preventDefault ( ) ;
41+ setIsDragOver ( false ) ;
42+
43+ const files = e . dataTransfer . files ;
44+ if ( files . length > 0 ) {
45+ onUpload ( files ) ;
46+ }
47+ } ;
48+
49+ const handleFileSelect = ( e : React . ChangeEvent < HTMLInputElement > ) => {
50+ const files = e . target . files ;
51+ if ( files && files . length > 0 ) {
52+ onUpload ( files ) ;
53+ }
54+ // Reset input value to allow selecting the same file again
55+ e . target . value = '' ;
56+ } ;
57+
58+ const formatFileSize = ( bytes : number ) => {
59+ if ( bytes === 0 ) return '0 Bytes' ;
60+ const k = 1024 ;
61+ const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ] ;
62+ const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
63+ return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
64+ } ;
65+
66+ const getFileIcon = ( mimeType : string ) => {
67+ if ( mimeType . startsWith ( 'image/' ) ) return '🖼️' ;
68+ if ( mimeType . startsWith ( 'video/' ) ) return '🎥' ;
69+ if ( mimeType . startsWith ( 'audio/' ) ) return '🎵' ;
70+ if ( mimeType . includes ( 'pdf' ) ) return '📄' ;
71+ if ( mimeType . includes ( 'document' ) || mimeType . includes ( 'word' ) ) return '📝' ;
72+ if ( mimeType . includes ( 'spreadsheet' ) || mimeType . includes ( 'excel' ) ) return '📊' ;
73+ return '📎' ;
74+ } ;
75+
76+ return (
77+ < div className = "space-y-4" >
78+ { /* Upload Area */ }
79+ < div
80+ className = { `border border-dashed rounded-lg p-3 text-center transition-colors ${
81+ isDragOver
82+ ? 'border-primary bg-primary/5'
83+ : 'border-border hover:border-primary/50'
84+ } `}
85+ onDragOver = { handleDragOver }
86+ onDragLeave = { handleDragLeave }
87+ onDrop = { handleDrop }
88+ >
89+ < input
90+ ref = { fileInputRef }
91+ type = "file"
92+ multiple
93+ className = "hidden"
94+ onChange = { handleFileSelect }
95+ />
96+
97+ < div className = "flex flex-col items-center gap-2" >
98+ < Upload className = "h-8 w-8 text-muted-foreground" />
99+ < p className = "text-sm text-muted-foreground" >
100+ Drag files here or{ ' ' }
101+ < button
102+ type = "button"
103+ className = "text-primary hover:underline"
104+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
105+ >
106+ browse
107+ </ button >
108+ </ p >
109+ </ div >
110+
111+ { isUploading && (
112+ < div className = "mt-4 space-y-2" >
113+ < div className = "flex items-center justify-center gap-2" >
114+ < Loader2 className = "h-4 w-4 animate-spin" />
115+ < span className = "text-sm" > Uploading...</ span >
116+ </ div >
117+ < Progress value = { uploadProgress } className = "w-full" />
118+ </ div >
119+ ) }
120+ </ div >
121+
122+ { /* Attachments List */ }
123+ { attachments . length > 0 && (
124+ < div className = "space-y-3" >
125+ < h4 className = "text-sm font-medium" > Attachments ({ attachments . length } )</ h4 >
126+ < div className = "flex flex-wrap gap-2" >
127+ { attachments . map ( ( attachment ) => {
128+ const isDeleting = deletingIds . includes ( attachment . id ) ;
129+
130+ return (
131+ < div
132+ key = { attachment . id }
133+ className = "flex items-center gap-2 p-2 border rounded-lg bg-card min-w-0 max-w-xs"
134+ >
135+ < div className = "text-sm" >
136+ { getFileIcon ( attachment . mimeType ) }
137+ </ div >
138+
139+ < div className = "flex-1 min-w-0" >
140+ < p className = "text-xs font-medium truncate" >
141+ { attachment . originalName }
142+ </ p >
143+ < p className = "text-xs text-muted-foreground" >
144+ { formatFileSize ( attachment . size ) }
145+ </ p >
146+ </ div >
147+
148+ < div className = "flex items-center gap-1" >
149+ < Button
150+ variant = "ghost"
151+ size = "sm"
152+ onClick = { ( ) => onDownload ( attachment ) }
153+ title = "Download"
154+ className = "h-6 w-6 p-0"
155+ disabled = { isDeleting }
156+ >
157+ < Download className = "h-3 w-3" />
158+ </ Button >
159+
160+ < Button
161+ variant = "ghost"
162+ size = "sm"
163+ onClick = { ( ) => onRemove ( attachment . id ) }
164+ className = "text-destructive hover:text-destructive h-6 w-6 p-0"
165+ title = "Remove"
166+ disabled = { isDeleting }
167+ >
168+ { isDeleting ? (
169+ < Loader2 className = "h-3 w-3 animate-spin" />
170+ ) : (
171+ < X className = "h-3 w-3" />
172+ ) }
173+ </ Button >
174+ </ div >
175+ </ div >
176+ ) ;
177+ } ) }
178+ </ div >
179+ </ div >
180+ ) }
181+ </ div >
182+ ) ;
183+ }
0 commit comments