Skip to content

Commit

Permalink
added support for create folder and file #4
Browse files Browse the repository at this point in the history
  • Loading branch information
cu8code committed Oct 16, 2024
1 parent 6bae63b commit fa90f02
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 20 deletions.
199 changes: 179 additions & 20 deletions src/components/FileExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { X } from "lucide-react";
import React, { useState, useEffect } from "react";
import { X, Plus } from "lucide-react";
import { useVSCodeStore } from "../store";
import { FileSystemTree } from "@webcontainer/api";

Expand All @@ -13,28 +13,71 @@ const FileExplorer: React.FC<FileExplorerProps> = ({ handleFileClick }) => {
selectedFile,
setShowExplorer,
getTheme,
}= useVSCodeStore();
createFile,
createFolder,
} = useVSCodeStore();
const theme = getTheme();
const [expandedFolders, setExpandedFolders] = useState<{ [key: string]: boolean }>({});
const [expandedFolders, setExpandedFolders] = useState<{
[key: string]: boolean;
}>({});
const [newFileName, setNewFileName] = useState("");
const [isFolder, setIsFolder] = useState(false);
const [currentPath, setCurrentPath] = useState(""); // Tracks the current path
const [showInput, setShowInput] = useState(false);
const [inputPositionPath, setInputPositionPath] = useState<string | null>(
null,
);
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
folderPath: string;
visible: boolean;
}>({
x: 0,
y: 0,
folderPath: "",
visible: false,
});

useEffect(() => {
const handleClickOutside = () => {
setContextMenu({ ...contextMenu, visible: false });
};

window.addEventListener("click", handleClickOutside);
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, [contextMenu]);

const getIcon = (fileName: string) => {
const extension = fileName.split(".").pop();
return (
theme.fileExplorer.body.icons[extension as keyof typeof theme.fileExplorer.body.icons] ||
theme.fileExplorer.body.icons["default-file"]
theme.fileExplorer.body.icons[
extension as keyof typeof theme.fileExplorer.body.icons
] || theme.fileExplorer.body.icons["default-file"]
);
};

const renderFileTree = (fileSystemTree: FileSystemTree, path = "", depth = 0) => {
const renderFileTree = (
fileSystemTree: FileSystemTree,
path = "",
depth = 0,
) => {
return Object.keys(fileSystemTree).map((fileName) => {
const filePath = `${path}/${fileName}`;
const filePathParts = filePath.split("/");
const displayName = filePathParts[filePathParts.length - 1];

if ("directory" in fileSystemTree[fileName]) {
const isExpanded = expandedFolders[filePath];
const isInputVisible = showInput && inputPositionPath === filePath;

return (
<div key={fileName}>
<div
key={fileName}
onContextMenu={(e) => handleContextMenu(e, filePath)}
>
<div
className="flex items-center cursor-pointer p-1"
style={{
Expand All @@ -53,13 +96,25 @@ const FileExplorer: React.FC<FileExplorerProps> = ({ handleFileClick }) => {
>
{theme.fileExplorer.body.icons["default-folder"]}
<span>{displayName}</span>
{isExpanded ? (
<span>&#8593;</span>
) : (
<span>&#8595;</span>
)}
{isExpanded ? <span>&#8593;</span> : <span>&#8595;</span>}
</div>
{isExpanded && renderFileTree(fileSystemTree[fileName].directory, filePath, depth + 1)}
{isExpanded &&
renderFileTree(
fileSystemTree[fileName].directory,
filePath,
depth + 1,
)}
{isInputVisible && (
<input
type="text"
value={newFileName}
onChange={handleInputChange}
onKeyDown={handleKeyPress}
placeholder="Enter file/folder name"
className="text-black mt-2 ml-8"
autoFocus
/>
)}
</div>
);
} else {
Expand All @@ -84,12 +139,74 @@ const FileExplorer: React.FC<FileExplorerProps> = ({ handleFileClick }) => {
});
};

const handleCreateFile = async (path: string, fileName: string) => {
await createFile(path, fileName, "");
setNewFileName("");
};

const handleCreateFolder = async (path: string, folderName: string) => {
await createFolder(path, folderName);
setNewFileName("");
setExpandedFolders((prev) => ({
...prev,
[`${path}/${folderName}`]: true,
}));
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewFileName(e.target.value);
setIsFolder(!e.target.value.includes("."));
};

const handleKeyPress = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
if (newFileName.trim() === "") {
setShowInput(false);
return;
}

if (isFolder) {
await handleCreateFolder(currentPath, newFileName);
} else {
await handleCreateFile(currentPath, newFileName);
}
setShowInput(false);
setInputPositionPath(null); // Reset input position
}
};

const handleContextMenu = (e: React.MouseEvent, folderPath: string) => {
e.preventDefault();
setContextMenu({
x: e.pageX,
y: e.pageY,
folderPath,
visible: true,
});
};

const handleMenuClick = (type: "file" | "folder") => {
setIsFolder(type === "folder");
setCurrentPath(contextMenu.folderPath);
setShowInput(true);
setInputPositionPath(contextMenu.folderPath); // Show input below the folder
setContextMenu({ ...contextMenu, visible: false });
};

// New: Add button handler for top-level file/folder creation
const handleAddButtonClick = (type: "file" | "folder") => {
setIsFolder(type === "folder");
setCurrentPath(""); // Root-level path or current folder
setShowInput(true);
setInputPositionPath(null); // Input appears at the root level
};

if (!files) {
return <div>Loading...</div>;
}

return (
<div className="flex h-full w-full flex-grow">
<div className="relative flex h-full w-full flex-grow">
<div
className="h-full w-full"
style={{
Expand All @@ -110,11 +227,18 @@ const FileExplorer: React.FC<FileExplorerProps> = ({ handleFileClick }) => {
}}
>
<h2 className="text-sm font-bold">Explorer</h2>
<X
size={18}
className="cursor-pointer"
onClick={() => setShowExplorer(false)}
/>
<div className="flex items-center">
<Plus
size={16}
className="cursor-pointer"
onClick={() => handleAddButtonClick("file")}
/>
<X
size={18}
className="cursor-pointer"
onClick={() => setShowExplorer(false)}
/>
</div>
</div>
<div
className="p-2"
Expand All @@ -123,9 +247,44 @@ const FileExplorer: React.FC<FileExplorerProps> = ({ handleFileClick }) => {
backgroundColor: theme.fileExplorer.body.backgroundColor,
}}
>
{/* Input field for creating new file/folder */}
{showInput && !inputPositionPath && (
<div className="p-2">
<input
type="text"
value={newFileName}
onChange={handleInputChange}
onKeyDown={handleKeyPress}
placeholder="Enter file/folder name"
className="text-black"
autoFocus
/>
</div>
)}
{renderFileTree(files)}
</div>
</div>

{/* Context menu for folder right-click */}
{contextMenu.visible && (
<div
className="absolute bg-white border shadow-md z-10"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
<div
className="p-2 cursor-pointer hover:bg-gray-200"
onClick={() => handleMenuClick("file")}
>
Add New File
</div>
<div
className="p-2 cursor-pointer hover:bg-gray-200"
onClick={() => handleMenuClick("folder")}
>
Add New Folder
</div>
</div>
)}
</div>
);
};
Expand Down
24 changes: 24 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface VSCodeState {
updateFile: (fileName: string, content: string) => void;
closeFile: (fileName: string) => void;
updateFileSystem: () => Promise<void>;
createFile: (filePath: string, filename: string, content: string) => Promise<void>;
createFolder: (filePath: string, filename: string) => Promise<void>;
}

export const useVSCodeStore = create<VSCodeState>((set, get) => ({
Expand Down Expand Up @@ -126,6 +128,28 @@ export const useVSCodeStore = create<VSCodeState>((set, get) => ({
const { theme, themes } = get();
return themes[theme];
},
createFile: async (filePath: string, fileName: string, content: string) => {
const { webcontainerInstance, updateFileSystem } = get();
if (webcontainerInstance) {
try {
await webcontainerInstance.fs.writeFile(`${filePath}/${fileName}`, content);
} catch (error) {
console.error(`Error creating file ${filePath}/${fileName}:`, error);
}
}
updateFileSystem()
},
createFolder: async (filePath: string, folderName: string) => {
const { webcontainerInstance, updateFileSystem } = get();
if (webcontainerInstance) {
try {
await webcontainerInstance.fs.mkdir(`${filePath}/${folderName}`);
} catch (error) {
console.error(`Error creating folder ${filePath}/${folderName}:`, error);
}
}
updateFileSystem()
},
}));

const readDir = async (
Expand Down

0 comments on commit fa90f02

Please sign in to comment.