Skip to content

Commit f5ddc4c

Browse files
author
Juan Castaño
committed
create folder nesting
1 parent 85761ca commit f5ddc4c

File tree

4 files changed

+276
-29
lines changed

4 files changed

+276
-29
lines changed

components/ui-builder/code-editor.tsx

Lines changed: 170 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,115 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import { benchifyFileSchema } from "@/lib/schemas";
33
import { z } from "zod";
44
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
55
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
66
import { cn } from "@/lib/utils";
77
import { ScrollArea } from "@/components/ui/scroll-area";
8+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
9+
import {
10+
ChevronDown,
11+
ChevronRight,
12+
Folder,
13+
FileText,
14+
FileCode,
15+
Settings,
16+
Palette,
17+
Globe,
18+
Package
19+
} from "lucide-react";
820

921
interface CodeEditorProps {
1022
files: z.infer<typeof benchifyFileSchema>;
1123
}
1224

25+
interface FileNode {
26+
name: string;
27+
path: string;
28+
type: 'file' | 'folder';
29+
children?: FileNode[];
30+
content?: string;
31+
}
32+
1333
export function CodeEditor({ files = [] }: CodeEditorProps) {
14-
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
34+
const [selectedFilePath, setSelectedFilePath] = useState<string>('');
35+
const [openFolders, setOpenFolders] = useState<Set<string>>(new Set());
36+
37+
// Build file tree structure (pure function, no side effects)
38+
const buildFileTree = (files: Array<{ path: string; content: string }>): { tree: FileNode[], allFolders: string[] } => {
39+
const root: FileNode[] = [];
40+
const folderMap = new Map<string, FileNode>();
41+
const allFolders: string[] = [];
42+
43+
// Sort files to ensure consistent ordering
44+
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
45+
46+
sortedFiles.forEach(file => {
47+
const parts = file.path.split('/');
48+
let currentPath = '';
49+
let currentLevel = root;
50+
51+
for (let i = 0; i < parts.length; i++) {
52+
const part = parts[i];
53+
const isLast = i === parts.length - 1;
54+
currentPath = currentPath ? `${currentPath}/${part}` : part;
55+
56+
if (isLast) {
57+
// It's a file
58+
currentLevel.push({
59+
name: part,
60+
path: file.path,
61+
type: 'file',
62+
content: file.content
63+
});
64+
} else {
65+
// It's a folder
66+
let folder = folderMap.get(currentPath);
67+
if (!folder) {
68+
folder = {
69+
name: part,
70+
path: currentPath,
71+
type: 'folder',
72+
children: []
73+
};
74+
folderMap.set(currentPath, folder);
75+
currentLevel.push(folder);
76+
allFolders.push(currentPath);
77+
}
78+
currentLevel = folder.children!;
79+
}
80+
}
81+
});
82+
83+
return { tree: root, allFolders };
84+
};
85+
86+
const { tree: fileTree, allFolders } = buildFileTree(files);
87+
const selectedFile = files.find(f => f.path === selectedFilePath);
88+
89+
// Open all folders by default (only once when files change)
90+
useEffect(() => {
91+
if (allFolders.length > 0) {
92+
setOpenFolders(new Set(allFolders));
93+
}
94+
}, [files.length]); // Only trigger when files array changes
1595

16-
// Selected file data
17-
const selectedFile = files[selectedFileIndex];
96+
// Auto-select first file if none selected (only once when files change)
97+
useEffect(() => {
98+
if (!selectedFilePath && files.length > 0) {
99+
setSelectedFilePath(files[0].path);
100+
}
101+
}, [files.length, selectedFilePath]); // Dependency on selectedFilePath prevents loops
102+
103+
// Get file icon based on extension
104+
const getFileIcon = (path: string) => {
105+
if (path.endsWith('.tsx') || path.endsWith('.jsx')) return <FileCode className="h-4 w-4 text-blue-500" />;
106+
if (path.endsWith('.ts') || path.endsWith('.js')) return <FileCode className="h-4 w-4 text-yellow-500" />;
107+
if (path.endsWith('.css')) return <Palette className="h-4 w-4 text-pink-500" />;
108+
if (path.endsWith('.html')) return <Globe className="h-4 w-4 text-orange-500" />;
109+
if (path.endsWith('.json') || path.includes('config')) return <Settings className="h-4 w-4 text-gray-500" />;
110+
if (path.includes('package.json')) return <Package className="h-4 w-4 text-green-500" />;
111+
return <FileText className="h-4 w-4 text-gray-400" />;
112+
};
18113

19114
// Determine file language for syntax highlighting
20115
const getLanguage = (path: string) => {
@@ -30,32 +125,74 @@ export function CodeEditor({ files = [] }: CodeEditorProps) {
30125
return 'text';
31126
};
32127

33-
// Extract filename from path
34-
const getFileName = (path: string) => {
35-
const parts = path.split('/');
36-
return parts[parts.length - 1];
128+
const toggleFolder = (folderPath: string) => {
129+
setOpenFolders(prev => {
130+
const newSet = new Set(prev);
131+
if (newSet.has(folderPath)) {
132+
newSet.delete(folderPath);
133+
} else {
134+
newSet.add(folderPath);
135+
}
136+
return newSet;
137+
});
138+
};
139+
140+
const renderFileTree = (nodes: FileNode[], depth = 0) => {
141+
return nodes.map(node => {
142+
if (node.type === 'folder') {
143+
const isOpen = openFolders.has(node.path);
144+
145+
return (
146+
<Collapsible key={node.path} open={isOpen} onOpenChange={() => toggleFolder(node.path)}>
147+
<CollapsibleTrigger className="flex items-center w-full text-left py-1 px-2 hover:bg-muted/50 text-sm">
148+
<div className="flex items-center gap-2" style={{ paddingLeft: `${depth * 12}px` }}>
149+
{isOpen ? (
150+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
151+
) : (
152+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
153+
)}
154+
<Folder className="h-4 w-4 text-blue-400" />
155+
<span className="font-medium">{node.name}</span>
156+
</div>
157+
</CollapsibleTrigger>
158+
<CollapsibleContent>
159+
{node.children && renderFileTree(node.children, depth + 1)}
160+
</CollapsibleContent>
161+
</Collapsible>
162+
);
163+
} else {
164+
const isSelected = selectedFilePath === node.path;
165+
166+
return (
167+
<button
168+
key={node.path}
169+
onClick={() => setSelectedFilePath(node.path)}
170+
className={cn(
171+
"flex items-center w-full text-left py-1.5 px-2 hover:bg-muted/50 text-sm transition-colors",
172+
isSelected && "bg-primary/10 text-primary"
173+
)}
174+
>
175+
<div className="flex items-center gap-2" style={{ paddingLeft: `${depth * 12 + 20}px` }}>
176+
{getFileIcon(node.path)}
177+
<span className="truncate">{node.name}</span>
178+
</div>
179+
</button>
180+
);
181+
}
182+
});
37183
};
38184

39185
return (
40-
<div className="grid grid-cols-[180px_1fr] h-[700px] gap-4">
41-
{/* File sidebar */}
186+
<div className="grid grid-cols-[320px_1fr] h-[700px] gap-4">
187+
{/* File sidebar - now wider */}
42188
<div className="border rounded-md overflow-hidden bg-card min-w-0 h-full">
43-
<div className="p-2 border-b bg-muted/50 font-medium text-sm">Files</div>
44-
<ScrollArea className="h-[calc(100%-33px)]">
45-
<div className="py-1">
46-
{files.map((file, index) => (
47-
<button
48-
key={file.path}
49-
onClick={() => setSelectedFileIndex(index)}
50-
className={cn(
51-
"w-full text-left px-2 py-1.5 text-xs hover:bg-muted/50",
52-
selectedFileIndex === index && "bg-primary/10 text-primary font-medium"
53-
)}
54-
title={file.path}
55-
>
56-
<span className="block truncate">{file.path}</span>
57-
</button>
58-
))}
189+
<div className="p-3 border-b bg-muted/50 font-semibold text-sm flex items-center gap-2">
190+
<Folder className="h-4 w-4" />
191+
Files
192+
</div>
193+
<ScrollArea className="h-[calc(100%-41px)]">
194+
<div className="py-2">
195+
{renderFileTree(fileTree)}
59196
</div>
60197
</ScrollArea>
61198
</div>
@@ -64,8 +201,9 @@ export function CodeEditor({ files = [] }: CodeEditorProps) {
64201
<div className="border rounded-md overflow-hidden h-full min-w-0 flex-1">
65202
{selectedFile ? (
66203
<div className="flex flex-col h-full">
67-
<div className="p-2 border-b bg-muted/50 font-medium flex items-center">
68-
<span className="text-sm truncate">{getFileName(selectedFile.path)}</span>
204+
<div className="p-3 border-b bg-muted/50 font-medium flex items-center gap-2">
205+
{getFileIcon(selectedFile.path)}
206+
<span className="text-sm truncate">{selectedFile.path}</span>
69207
</div>
70208
<SyntaxHighlighter
71209
language={getLanguage(selectedFile.path)}
@@ -104,7 +242,10 @@ export function CodeEditor({ files = [] }: CodeEditorProps) {
104242
</div>
105243
) : (
106244
<div className="flex items-center justify-center h-full text-muted-foreground">
107-
No file selected
245+
<div className="text-center">
246+
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
247+
<p>Select a file to view its contents</p>
248+
</div>
108249
</div>
109250
)}
110251
</div>

components/ui/collapsible.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client"
2+
3+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4+
5+
function Collapsible({
6+
...props
7+
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9+
}
10+
11+
function CollapsibleTrigger({
12+
...props
13+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14+
return (
15+
<CollapsiblePrimitive.CollapsibleTrigger
16+
data-slot="collapsible-trigger"
17+
{...props}
18+
/>
19+
)
20+
}
21+
22+
function CollapsibleContent({
23+
...props
24+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25+
return (
26+
<CollapsiblePrimitive.CollapsibleContent
27+
data-slot="collapsible-content"
28+
{...props}
29+
/>
30+
)
31+
}
32+
33+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

package-lock.json

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@e2b/code-interpreter": "^1.5.0",
1414
"@e2b/sdk": "^0.12.5",
1515
"@hookform/resolvers": "^5.0.1",
16+
"@radix-ui/react-collapsible": "^1.1.11",
1617
"@radix-ui/react-label": "^2.1.6",
1718
"@radix-ui/react-scroll-area": "^1.2.8",
1819
"@radix-ui/react-slot": "^1.2.2",

0 commit comments

Comments
 (0)