1+ import { AnimatePresence , motion } from 'framer-motion'
12import { Edit , File , Plus , Trash } from 'lucide-react'
23import { useEffect , useState } from 'react'
34
@@ -13,6 +14,7 @@ import {
1314 TreeView ,
1415 TreeViewItem ,
1516} from 'ui'
17+ import { getLanguageFromFileName , isBinaryFile } from './FileExplorerAndEditor.utils'
1618
1719interface FileData {
1820 id : number
@@ -32,35 +34,16 @@ interface FileExplorerAndEditorProps {
3234 }
3335}
3436
35- const getLanguageFromFileName = ( fileName : string ) : string => {
36- const extension = fileName . split ( '.' ) . pop ( ) ?. toLowerCase ( )
37- switch ( extension ) {
38- case 'ts' :
39- case 'tsx' :
40- return 'typescript'
41- case 'js' :
42- case 'jsx' :
43- return 'javascript'
44- case 'json' :
45- return 'json'
46- case 'html' :
47- return 'html'
48- case 'css' :
49- return 'css'
50- case 'md' :
51- return 'markdown'
52- default :
53- return 'typescript' // Default to typescript
54- }
55- }
37+ const denoJsonDefaultContent = JSON . stringify ( { imports : { } } , null , '\t' )
5638
57- const FileExplorerAndEditor = ( {
39+ export const FileExplorerAndEditor = ( {
5840 files,
5941 onFilesChange,
6042 aiEndpoint,
6143 aiMetadata,
6244} : FileExplorerAndEditorProps ) => {
6345 const selectedFile = files . find ( ( f ) => f . selected ) ?? files [ 0 ]
46+ const [ isDragOver , setIsDragOver ] = useState ( false )
6447
6548 const [ treeData , setTreeData ] = useState ( {
6649 name : '' ,
@@ -95,9 +78,55 @@ const FileExplorerAndEditor = ({
9578 ] )
9679 }
9780
81+ const addDroppedFiles = async ( droppedFiles : FileList ) => {
82+ const newFiles : FileData [ ] = [ ]
83+ const updatedFiles = files . map ( ( f ) => ( { ...f , selected : false } ) )
84+
85+ for ( let i = 0 ; i < droppedFiles . length ; i ++ ) {
86+ const file = droppedFiles [ i ]
87+ const newId = Math . max ( 0 , ...files . map ( ( f ) => f . id ) , ...newFiles . map ( ( f ) => f . id ) ) + 1
88+
89+ try {
90+ let content : string
91+ if ( isBinaryFile ( file . name ) ) {
92+ // For binary files, read as ArrayBuffer and convert to base64 or keep as binary data
93+ const arrayBuffer = await file . arrayBuffer ( )
94+ const bytes = new Uint8Array ( arrayBuffer )
95+ content = Array . from ( bytes , ( byte ) => String . fromCharCode ( byte ) ) . join ( '' )
96+ } else {
97+ content = await file . text ( )
98+ }
99+
100+ newFiles . push ( {
101+ id : newId ,
102+ name : file . name ,
103+ content,
104+ selected : i === droppedFiles . length - 1 , // Select the last dropped file
105+ } )
106+ } catch ( error ) {
107+ console . error ( `Failed to read file ${ file . name } :` , error )
108+ }
109+ }
110+
111+ if ( newFiles . length > 0 ) {
112+ onFilesChange ( [ ...updatedFiles , ...newFiles ] )
113+ }
114+ }
115+
98116 const handleFileNameChange = ( id : number , newName : string ) => {
99117 if ( ! newName . trim ( ) ) return // Don't allow empty names
100- const updatedFiles = files . map ( ( file ) => ( file . id === id ? { ...file , name : newName } : file ) )
118+ const updatedFiles = files . map ( ( file ) =>
119+ file . id === id
120+ ? {
121+ ...file ,
122+ name : newName ,
123+ content :
124+ newName === 'deno.json' && file . content === ''
125+ ? denoJsonDefaultContent
126+ : file . content ,
127+ }
128+ : file
129+ )
101130 onFilesChange ( updatedFiles )
102131 }
103132
@@ -145,6 +174,26 @@ const FileExplorerAndEditor = ({
145174 setTreeData ( updatedTreeData )
146175 }
147176
177+ const handleDragOver = ( e : React . DragEvent ) => {
178+ e . preventDefault ( )
179+ setIsDragOver ( true )
180+ }
181+
182+ const handleDragLeave = ( e : React . DragEvent ) => {
183+ e . preventDefault ( )
184+ setIsDragOver ( false )
185+ }
186+
187+ const handleDrop = async ( e : React . DragEvent ) => {
188+ e . preventDefault ( )
189+ setIsDragOver ( false )
190+
191+ const droppedFiles = e . dataTransfer . files
192+ if ( droppedFiles . length > 0 ) {
193+ await addDroppedFiles ( droppedFiles )
194+ }
195+ }
196+
148197 // Update treeData when files change
149198 useEffect ( ( ) => {
150199 setTreeData ( {
@@ -161,7 +210,27 @@ const FileExplorerAndEditor = ({
161210 } , [ files ] )
162211
163212 return (
164- < div className = "flex-1 overflow-hidden flex h-full" >
213+ < div
214+ className = { `flex-1 overflow-hidden flex h-full relative ${ isDragOver ? 'bg-blue-50' : '' } ` }
215+ onDragOver = { handleDragOver }
216+ onDragLeave = { handleDragLeave }
217+ onDrop = { handleDrop }
218+ >
219+ < AnimatePresence >
220+ { isDragOver && (
221+ < motion . div
222+ initial = { { opacity : 0 } }
223+ animate = { { opacity : 1 } }
224+ exit = { { opacity : 0 } }
225+ transition = { { duration : 0.1 } }
226+ className = "absolute inset-0 bg bg-opacity-30 z-10 flex items-center justify-center"
227+ >
228+ < div className = "w-96 py-20 bg bg-opacity-60 border-2 border-dashed border-muted flex items-center justify-center" >
229+ < div className = "text-base" > Drop files here to add them</ div >
230+ </ div >
231+ </ motion . div >
232+ ) }
233+ </ AnimatePresence >
165234 < div className = "w-64 border-r bg-surface-200 flex flex-col" >
166235 < div className = "py-4 px-6 border-b flex items-center justify-between" >
167236 < h3 className = "text-sm font-normal font-mono uppercase text-lighter tracking-wide" >
@@ -197,13 +266,17 @@ const FileExplorerAndEditor = ({
197266 icon = { < File size = { 14 } className = "text-foreground-light shrink-0" /> }
198267 isEditing = { Boolean ( element . metadata ?. isEditing ) }
199268 onEditSubmit = { ( value ) => {
200- if ( originalId !== null ) handleFileNameChange ( originalId , value )
269+ if ( originalId !== null ) {
270+ handleFileNameChange ( originalId , value )
271+ }
201272 } }
202273 onClick = { ( ) => {
203274 if ( originalId !== null ) handleFileSelect ( originalId )
204275 } }
205276 onDoubleClick = { ( ) => {
206- if ( originalId !== null ) handleStartRename ( originalId )
277+ if ( originalId !== null ) {
278+ handleStartRename ( originalId )
279+ }
207280 } }
208281 />
209282 </ div >
@@ -226,7 +299,9 @@ const FileExplorerAndEditor = ({
226299 < ContextMenuItem_Shadcn_
227300 className = "gap-x-2"
228301 onSelect = { ( ) => {
229- if ( originalId !== null ) handleFileDelete ( originalId )
302+ if ( originalId !== null ) {
303+ handleFileDelete ( originalId )
304+ }
230305 } }
231306 onFocusCapture = { ( e ) => e . stopPropagation ( ) }
232307 >
@@ -243,26 +318,36 @@ const FileExplorerAndEditor = ({
243318 </ div >
244319 </ div >
245320 < div className = "flex-1 min-h-0 relative px-3 bg-surface-200" >
246- < AIEditor
247- language = { getLanguageFromFileName ( selectedFile ?. name || 'index.ts' ) }
248- value = { selectedFile ?. content }
249- onChange = { handleChange }
250- aiEndpoint = { aiEndpoint }
251- aiMetadata = { aiMetadata }
252- options = { {
253- tabSize : 2 ,
254- fontSize : 13 ,
255- minimap : { enabled : false } ,
256- wordWrap : 'on' ,
257- lineNumbers : 'on' ,
258- folding : false ,
259- padding : { top : 20 , bottom : 20 } ,
260- lineNumbersMinChars : 3 ,
261- } }
262- />
321+ { selectedFile && isBinaryFile ( selectedFile . name ) ? (
322+ < div className = "flex items-center justify-center h-full" >
323+ < div className = "text-center" >
324+ < div className = "text-foreground-light text-lg mb-2" > Cannot Edit Selected File</ div >
325+ < div className = "text-foreground-lighter text-sm" >
326+ Binary files like .{ selectedFile . name . split ( '.' ) . pop ( ) } cannot be edited in the text
327+ editor
328+ </ div >
329+ </ div >
330+ </ div >
331+ ) : (
332+ < AIEditor
333+ language = { getLanguageFromFileName ( selectedFile ?. name || 'index.ts' ) }
334+ value = { selectedFile ?. content }
335+ onChange = { handleChange }
336+ aiEndpoint = { aiEndpoint }
337+ aiMetadata = { aiMetadata }
338+ options = { {
339+ tabSize : 2 ,
340+ fontSize : 13 ,
341+ minimap : { enabled : false } ,
342+ wordWrap : 'on' ,
343+ lineNumbers : 'on' ,
344+ folding : false ,
345+ padding : { top : 20 , bottom : 20 } ,
346+ lineNumbersMinChars : 3 ,
347+ } }
348+ />
349+ ) }
263350 </ div >
264351 </ div >
265352 )
266353}
267-
268- export default FileExplorerAndEditor
0 commit comments