1- import React , { useRef } from 'react' ;
1+ import React , { useRef , useState , useCallback } from 'react' ;
22import { Button } from '@object-ui/components' ;
3- import { Upload , X , File as FileIcon } from 'lucide-react' ;
3+ import { Upload , X , File as FileIcon , ImageIcon } from 'lucide-react' ;
44import { FieldWidgetProps } from './types' ;
55
66/**
@@ -12,6 +12,7 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
1212 const fileField = ( field || ( props as any ) . schema ) as any ;
1313 const multiple = fileField ?. multiple || false ;
1414 const accept = fileField ?. accept ? fileField . accept . join ( ',' ) : undefined ;
15+ const [ isDragOver , setIsDragOver ] = useState ( false ) ;
1516
1617 if ( readonly ) {
1718 if ( ! value ) return < span className = "text-sm" > -</ span > ;
@@ -30,8 +31,7 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
3031
3132 const files = value ? ( Array . isArray ( value ) ? value : [ value ] ) : [ ] ;
3233
33- const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
34- const selectedFiles = Array . from ( e . target . files || [ ] ) ;
34+ const processFiles = useCallback ( ( selectedFiles : File [ ] ) => {
3535 if ( selectedFiles . length === 0 ) return ;
3636
3737 const fileObjects = selectedFiles . map ( file => ( {
@@ -48,8 +48,44 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
4848 } else {
4949 onChange ( fileObjects [ 0 ] ) ;
5050 }
51+ } , [ files , multiple , onChange ] ) ;
52+
53+ const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
54+ processFiles ( Array . from ( e . target . files || [ ] ) ) ;
5155 } ;
5256
57+ const handleDragOver = useCallback ( ( e : React . DragEvent ) => {
58+ e . preventDefault ( ) ;
59+ e . stopPropagation ( ) ;
60+ setIsDragOver ( true ) ;
61+ } , [ ] ) ;
62+
63+ const handleDragLeave = useCallback ( ( e : React . DragEvent ) => {
64+ e . preventDefault ( ) ;
65+ e . stopPropagation ( ) ;
66+ setIsDragOver ( false ) ;
67+ } , [ ] ) ;
68+
69+ const handleDrop = useCallback ( ( e : React . DragEvent ) => {
70+ e . preventDefault ( ) ;
71+ e . stopPropagation ( ) ;
72+ setIsDragOver ( false ) ;
73+
74+ const droppedFiles = Array . from ( e . dataTransfer . files ) ;
75+ if ( accept ) {
76+ const acceptedTypes = accept . split ( ',' ) . map ( t => t . trim ( ) . toLowerCase ( ) ) ;
77+ const filtered = droppedFiles . filter ( file => {
78+ const ext = '.' + file . name . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ;
79+ return acceptedTypes . some ( t =>
80+ t === file . type || t === ext || ( t . endsWith ( '/*' ) && file . type . startsWith ( t . replace ( '/*' , '/' ) ) )
81+ ) ;
82+ } ) ;
83+ processFiles ( filtered ) ;
84+ } else {
85+ processFiles ( droppedFiles ) ;
86+ }
87+ } , [ accept , processFiles ] ) ;
88+
5389 const handleRemove = ( index : number ) => {
5490 if ( multiple ) {
5591 const newFiles = files . filter ( ( _ : any , i : number ) => i !== index ) ;
@@ -59,6 +95,11 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
5995 }
6096 } ;
6197
98+ const isImage = ( file : any ) => {
99+ const mime = file . mime_type || '' ;
100+ return mime . startsWith ( 'image/' ) ;
101+ } ;
102+
62103 return (
63104 < div className = { props . className } >
64105 < input
@@ -71,20 +112,61 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
71112 />
72113
73114 < div className = "space-y-2" >
115+ { /* Drag-and-drop zone */ }
116+ < div
117+ onDragOver = { handleDragOver }
118+ onDragLeave = { handleDragLeave }
119+ onDrop = { handleDrop }
120+ onClick = { ( ) => inputRef . current ?. click ( ) }
121+ className = { `
122+ flex flex-col items-center justify-center gap-2 p-6
123+ border-2 border-dashed rounded-lg cursor-pointer
124+ transition-colors duration-200
125+ ${ isDragOver
126+ ? 'border-primary bg-primary/5 text-primary'
127+ : 'border-muted-foreground/25 hover:border-primary/50 text-muted-foreground hover:text-foreground' }
128+ ` }
129+ role = "button"
130+ tabIndex = { 0 }
131+ onKeyDown = { ( e ) => {
132+ if ( e . key === 'Enter' || e . key === ' ' ) {
133+ e . preventDefault ( ) ;
134+ inputRef . current ?. click ( ) ;
135+ }
136+ } }
137+ >
138+ < Upload className = { `size-8 ${ isDragOver ? 'text-primary' : 'text-muted-foreground' } ` } />
139+ < div className = "text-center" >
140+ < p className = "text-sm font-medium" >
141+ { isDragOver ? 'Drop files here' : 'Drag & drop files here' }
142+ </ p >
143+ < p className = "text-xs text-muted-foreground mt-1" >
144+ or click to browse
145+ </ p >
146+ </ div >
147+ </ div >
148+
149+ { /* File list */ }
74150 { files . length > 0 && (
75151 < div className = "space-y-1" >
76152 { files . map ( ( file : any , idx : number ) => (
77153 < div
78154 key = { idx }
79- className = "flex items-center justify-between gap-2 p-2 bg-gray- 50 rounded-md border border-gray-200 "
155+ className = "flex items-center justify-between gap-2 p-2 bg-muted/ 50 rounded-md border"
80156 >
81157 < div className = "flex items-center gap-2 flex-1 min-w-0" >
82- < FileIcon className = "size-4 text-gray-500 flex-shrink-0" />
158+ { isImage ( file ) && file . url ? (
159+ < img src = { file . url } alt = { file . name } className = "size-8 object-cover rounded flex-shrink-0" />
160+ ) : isImage ( file ) ? (
161+ < ImageIcon className = "size-4 text-muted-foreground flex-shrink-0" />
162+ ) : (
163+ < FileIcon className = "size-4 text-muted-foreground flex-shrink-0" />
164+ ) }
83165 < span className = "text-sm truncate" >
84166 { file . name || file . original_name || 'File' }
85167 </ span >
86168 { file . size && (
87- < span className = "text-xs text-gray-500 " >
169+ < span className = "text-xs text-muted-foreground " >
88170 ({ ( file . size / 1024 ) . toFixed ( 1 ) } KB)
89171 </ span >
90172 ) }
@@ -93,7 +175,10 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
93175 type = "button"
94176 variant = "ghost"
95177 size = "sm"
96- onClick = { ( ) => handleRemove ( idx ) }
178+ onClick = { ( e ) => {
179+ e . stopPropagation ( ) ;
180+ handleRemove ( idx ) ;
181+ } }
97182 className = "h-6 w-6 p-0"
98183 >
99184 < X className = "size-3" />
@@ -102,16 +187,6 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
102187 ) ) }
103188 </ div >
104189 ) }
105-
106- < Button
107- type = "button"
108- variant = "outline"
109- onClick = { ( ) => inputRef . current ?. click ( ) }
110- className = "w-full"
111- >
112- < Upload className = "size-4 mr-2" />
113- { files . length > 0 ? 'Add More Files' : 'Upload File' }
114- </ Button >
115190 </ div >
116191 </ div >
117192 ) ;
0 commit comments