1- import { useState } from 'react'
2- import {
3- ChevronDown ,
4- ChevronRight ,
5- File as FileIcon ,
6- Folder ,
7- Plus ,
8- Trash2 ,
9- FolderPlus ,
10- } from 'lucide-react'
11- import { Button } from './ui/button'
12- import {
13- Dialog ,
14- DialogContent ,
15- DialogHeader ,
16- DialogTitle ,
17- DialogTrigger ,
18- } from './ui/dialog'
1+ import { useState } from 'react'
2+ import {
3+ ChevronDown ,
4+ ChevronRight ,
5+ File as FileIcon ,
6+ Folder ,
7+ Plus ,
8+ Trash2 ,
9+ FolderPlus ,
10+ } from 'lucide-react'
11+ import { Button } from './ui/button'
12+ import {
13+ Dialog ,
14+ DialogContent ,
15+ DialogHeader ,
16+ DialogTitle ,
17+ DialogTrigger ,
18+ } from './ui/dialog'
1919import { Input } from './ui/input'
2020
2121export interface FileSystemNode {
@@ -24,173 +24,172 @@ export interface FileSystemNode {
2424 children ?: FileSystemNode [ ]
2525}
2626
27- interface FileTreeProps {
28- files : FileSystemNode [ ]
29- onSelectFile : ( path : string ) => void
30- onCreateFile ?: ( path : string , isDirectory : boolean ) => void
31- onDeleteFile ?: ( path : string ) => void
32- }
33-
34- export function FileTree ( { files, onSelectFile, onCreateFile, onDeleteFile } : FileTreeProps ) {
35- const [ newFileName , setNewFileName ] = useState ( '' )
36- const [ isCreateDialogOpen , setIsCreateDialogOpen ] = useState ( false )
37- const [ createType , setCreateType ] = useState < 'file' | 'folder' > ( 'file' )
38-
39- const handleCreateFile = ( ) => {
40- if ( newFileName . trim ( ) && onCreateFile ) {
41- onCreateFile ( newFileName . trim ( ) , createType === 'folder' )
42- setNewFileName ( '' )
43- setIsCreateDialogOpen ( false )
44- }
45- }
46-
47- return (
48- < div className = "p-2" >
49- < div className = "flex items-center justify-between mb-2" >
50- < span className = "text-sm font-medium text-muted-foreground" > Files</ span >
51- < div className = "flex gap-1" >
52- < Dialog open = { isCreateDialogOpen } onOpenChange = { setIsCreateDialogOpen } >
53- < DialogTrigger asChild >
54- < Button
55- variant = "ghost"
56- size = "icon"
57- className = "h-6 w-6"
58- onClick = { ( ) => setCreateType ( 'file' ) }
59- >
60- < Plus className = "h-3 w-3" />
61- </ Button >
62- </ DialogTrigger >
63- < DialogContent className = "sm:max-w-md" >
64- < DialogHeader >
65- < DialogTitle > Create New { createType === 'file' ? 'File' : 'Folder' } </ DialogTitle >
66- </ DialogHeader >
67- < div className = "flex items-center space-x-2" >
68- < Input
69- placeholder = { createType === 'file' ? 'filename.ts' : 'folder-name' }
70- value = { newFileName }
71- onChange = { ( e ) => setNewFileName ( e . target . value ) }
72- onKeyDown = { ( e ) => {
73- if ( e . key === 'Enter' ) {
74- handleCreateFile ( )
75- }
76- } }
77- />
78- < Button onClick = { handleCreateFile } > Create</ Button >
79- </ div >
80- < div className = "flex gap-2 mt-2" >
81- < Button
82- variant = { createType === 'file' ? 'default' : 'outline' }
83- size = "sm"
84- onClick = { ( ) => setCreateType ( 'file' ) }
85- >
86- < FileIcon className = "h-3 w-3 mr-1" />
87- File
88- </ Button >
89- < Button
90- variant = { createType === 'folder' ? 'default' : 'outline' }
91- size = "sm"
92- onClick = { ( ) => setCreateType ( 'folder' ) }
93- >
94- < FolderPlus className = "h-3 w-3 mr-1" />
95- Folder
96- </ Button >
97- </ div >
98- </ DialogContent >
99- </ Dialog >
100- </ div >
101- </ div >
102- { files . map ( file => (
103- < FileTreeNode
104- key = { file . name }
105- node = { file }
106- onSelectFile = { onSelectFile }
107- onDeleteFile = { onDeleteFile }
108- />
109- ) ) }
110- </ div >
111- )
27+ interface FileTreeProps {
28+ files : FileSystemNode [ ]
29+ onSelectFile : ( path : string ) => void
30+ onCreateFile ?: ( path : string , isDirectory : boolean ) => void
31+ onDeleteFile ?: ( path : string ) => void
11232}
11333
114- interface FileTreeNodeProps {
115- node : FileSystemNode
116- onSelectFile : ( path : string ) => void
117- onDeleteFile ?: ( path : string ) => void
118- level ?: number
119- }
120-
121- function FileTreeNode ( {
122- node,
123- onSelectFile,
124- onDeleteFile,
125- level = 0 ,
126- path = '' ,
127- } : FileTreeNodeProps & { path ?: string } ) {
128- const [ isOpen , setIsOpen ] = useState ( false )
129-
130- const isDirectory = node . isDirectory
131- const hasChildren = node . children && node . children . length > 0
132- const newPath = `${ path } /${ node . name } `
133-
134- const handleToggle = ( ) => {
135- if ( isDirectory ) {
136- setIsOpen ( ! isOpen )
137- } else {
138- onSelectFile ( newPath )
139- }
140- }
141-
142- const handleDelete = ( ) => {
143- if ( onDeleteFile ) {
144- onDeleteFile ( newPath )
145- }
146- }
147-
148- return (
149- < div >
150- < div
151- className = "flex items-center cursor-pointer hover:bg-muted/50 rounded-sm p-1 group"
152- style = { { paddingLeft : level * 16 + 4 } }
153- onClick = { handleToggle }
154- >
155- { isDirectory ? (
156- < >
157- { isOpen ? (
158- < ChevronDown size = { 16 } className = "mr-1 text-muted-foreground" />
159- ) : (
160- < ChevronRight size = { 16 } className = "mr-1 text-muted-foreground" />
161- ) }
162- < Folder size = { 16 } className = "mr-2 text-blue-500" />
163- </ >
164- ) : (
165- < FileIcon size = { 16 } className = "mr-2 ml-4 text-muted-foreground" />
166- ) }
167- < span className = "text-sm truncate flex-1" > { node . name } </ span >
168- { onDeleteFile && (
169- < Button
170- variant = "ghost"
171- size = "icon"
172- className = "h-4 w-4 opacity-0 group-hover:opacity-100 ml-1"
173- onClick = { ( e ) => {
174- e . stopPropagation ( )
175- handleDelete ( )
176- } }
177- >
178- < Trash2 className = "h-3 w-3 text-red-500" />
179- </ Button >
180- ) }
181- </ div >
182- { isOpen &&
183- hasChildren &&
184- node . children ?. map ( child => (
185- < FileTreeNode
186- key = { child . name }
187- node = { child }
188- onSelectFile = { onSelectFile }
189- onDeleteFile = { onDeleteFile }
190- level = { level + 1 }
191- path = { newPath }
192- />
193- ) ) }
194- </ div >
195- )
34+ export function FileTree ( { files, onSelectFile, onCreateFile, onDeleteFile } : FileTreeProps ) {
35+ const [ newFileName , setNewFileName ] = useState ( '' )
36+ const [ isCreateDialogOpen , setIsCreateDialogOpen ] = useState ( false )
37+ const [ createType , setCreateType ] = useState < 'file' | 'folder' > ( 'file' )
38+
39+ const handleCreateFile = ( ) => {
40+ if ( newFileName . trim ( ) && onCreateFile ) {
41+ onCreateFile ( newFileName . trim ( ) , createType === 'folder' )
42+ setNewFileName ( '' )
43+ setIsCreateDialogOpen ( false )
44+ }
45+ }
46+
47+ return (
48+ < div className = "p-2" >
49+ < div className = "flex items-center justify-between mb-2" >
50+ < span className = "text-sm font-medium text-muted-foreground" > Files</ span >
51+ < div className = "flex gap-1" >
52+ < Dialog open = { isCreateDialogOpen } onOpenChange = { setIsCreateDialogOpen } >
53+ < DialogTrigger asChild >
54+ < Button
55+ variant = "ghost"
56+ size = "icon"
57+ className = "h-6 w-6"
58+ >
59+ < Plus className = "h-3 w-3" />
60+ </ Button >
61+ </ DialogTrigger >
62+ < DialogContent className = "sm:max-w-md" >
63+ < DialogHeader >
64+ < DialogTitle > Create New { createType === 'file' ? 'File' : 'Folder' } </ DialogTitle >
65+ </ DialogHeader >
66+ < div className = "flex items-center space-x-2" >
67+ < Input
68+ placeholder = { createType === 'file' ? 'filename.ts' : 'folder-name' }
69+ value = { newFileName }
70+ onChange = { ( e ) => setNewFileName ( e . target . value ) }
71+ onKeyDown = { ( e ) => {
72+ if ( e . key === 'Enter' ) {
73+ handleCreateFile ( )
74+ }
75+ } }
76+ />
77+ < Button onClick = { handleCreateFile } > Create</ Button >
78+ </ div >
79+ < div className = "flex gap-2 mt-2" >
80+ < Button
81+ variant = { createType === 'file' ? 'default' : 'outline' }
82+ size = "sm"
83+ onClick = { ( ) => setCreateType ( 'file' ) }
84+ >
85+ < FileIcon className = "h-3 w-3 mr-1" />
86+ File
87+ </ Button >
88+ < Button
89+ variant = { createType === 'folder' ? 'default' : 'outline' }
90+ size = "sm"
91+ onClick = { ( ) => setCreateType ( 'folder' ) }
92+ >
93+ < FolderPlus className = "h-3 w-3 mr-1" />
94+ Folder
95+ </ Button >
96+ </ div >
97+ </ DialogContent >
98+ </ Dialog >
99+ </ div >
100+ </ div >
101+ { files . map ( file => (
102+ < FileTreeNode
103+ key = { file . name }
104+ node = { file }
105+ onSelectFile = { onSelectFile }
106+ onDeleteFile = { onDeleteFile }
107+ />
108+ ) ) }
109+ </ div >
110+ )
111+ }
112+
113+ interface FileTreeNodeProps {
114+ node : FileSystemNode
115+ onSelectFile : ( path : string ) => void
116+ onDeleteFile ?: ( path : string ) => void
117+ level ?: number
118+ }
119+
120+ function FileTreeNode ( {
121+ node,
122+ onSelectFile,
123+ onDeleteFile,
124+ level = 0 ,
125+ path = '' ,
126+ } : FileTreeNodeProps & { path ?: string } ) {
127+ const [ isOpen , setIsOpen ] = useState ( false )
128+
129+ const isDirectory = node . isDirectory
130+ const hasChildren = node . children && node . children . length > 0
131+ const newPath = `${ path } /${ node . name } `
132+
133+ const handleToggle = ( ) => {
134+ if ( isDirectory ) {
135+ setIsOpen ( ! isOpen )
136+ } else {
137+ onSelectFile ( newPath )
138+ }
139+ }
140+
141+ const handleDelete = ( ) => {
142+ if ( onDeleteFile ) {
143+ onDeleteFile ( newPath )
144+ }
145+ }
146+
147+ return (
148+ < div >
149+ < div
150+ className = "flex items-center cursor-pointer hover:bg-muted/50 rounded-sm p-1 group"
151+ style = { { paddingLeft : level * 16 + 4 } }
152+ onClick = { handleToggle }
153+ >
154+ { isDirectory ? (
155+ < >
156+ { isOpen ? (
157+ < ChevronDown size = { 16 } className = "mr-1 text-muted-foreground" />
158+ ) : (
159+ < ChevronRight size = { 16 } className = "mr-1 text-muted-foreground" />
160+ ) }
161+ < Folder size = { 16 } className = "mr-2 text-blue-500" />
162+ </ >
163+ ) : (
164+ < FileIcon size = { 16 } className = "mr-2 ml-4 text-muted-foreground" />
165+ ) }
166+ < span className = "text-sm truncate flex-1" > { node . name } </ span >
167+ { onDeleteFile && (
168+ < Button
169+ variant = "ghost"
170+ size = "icon"
171+ className = "h-4 w-4 opacity-0 group-hover:opacity-100 ml-1"
172+ onClick = { ( e ) => {
173+ e . stopPropagation ( )
174+ handleDelete ( )
175+ } }
176+ >
177+ < Trash2 className = "h-3 w-3 text-red-500" />
178+ </ Button >
179+ ) }
180+ </ div >
181+ { isOpen &&
182+ hasChildren &&
183+ node . children ?. map ( child => (
184+ < FileTreeNode
185+ key = { child . name }
186+ node = { child }
187+ onSelectFile = { onSelectFile }
188+ onDeleteFile = { onDeleteFile }
189+ level = { level + 1 }
190+ path = { newPath }
191+ />
192+ ) ) }
193+ </ div >
194+ )
196195}
0 commit comments