import React, { useState, useEffect } from "react"; import { Folder, File, ChevronRight, ChevronDown, GitCommit, X, GitBranch, RefreshCw, XCircle, AlertTriangle, Layout, Search, Settings, Home, Activity } from "lucide-react"; import Editor from "@monaco-editor/react"; import { cn } from "@/lib/utils"; interface WebIDEProps { owner: string; repo: string; defaultBranch: string; currentUser: any; } // Simple Tree structure type TreeNode = { name: string; path: string; type: "file" | "dir"; children?: TreeNode[]; isOpen?: boolean; }; export function WebIDE({ owner, repo, defaultBranch, currentUser }: WebIDEProps) { const [tree, setTree] = useState([]); const [loading, setLoading] = useState(true); const [openFiles, setOpenFiles] = useState<{path: string, content: string, originalContent: string, isModified: boolean}[]>([]); const [activeFile, setActiveFile] = useState(null); const [activeTab, setActiveTab] = useState<"explorer" | "search" | "scm">("explorer"); const [commitMessage, setCommitMessage] = useState(""); const [committing, setCommitting] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true); // Fetch tree useEffect(() => { const fetchTree = async () => { try { const res = await fetch(`/api/repos/${owner}/${repo}/ide/fs?action=tree&branch=${defaultBranch}`); const data = await res.json(); if (data.data?.files) { const paths: string[] = data.data.files; const root: TreeNode[] = []; paths.forEach(p => { const parts = p.split("/"); let currentLevel = root; let currentPath = ""; parts.forEach((part, idx) => { currentPath += (currentPath ? "/" : "") + part; const isFile = idx === parts.length - 1; let node = currentLevel.find(n => n.name === part); if (!node) { node = { name: part, path: currentPath, type: isFile ? "file" : "dir", children: isFile ? undefined : [], isOpen: false }; currentLevel.push(node); } if (!isFile) { currentLevel = node.children!; } }); }); // Sort: dirs first const sortTree = (nodes: TreeNode[]) => { nodes.sort((a, b) => { if (a.type !== b.type) return a.type === "dir" ? -1 : 1; return a.name.localeCompare(b.name); }); nodes.forEach(n => n.children && sortTree(n.children)); }; sortTree(root); setTree(root); } } catch (e) { console.error(e); } finally { setLoading(false); } }; fetchTree(); }, [owner, repo, defaultBranch]); const handleOpenFile = async (path: string) => { if (!openFiles.find(f => f.path === path)) { // fetch content try { const res = await fetch(`/api/repos/${owner}/${repo}/ide/fs?action=file&branch=${defaultBranch}&path=${encodeURIComponent(path)}`); const data = await res.json(); if (data.data?.content !== undefined) { setOpenFiles(prev => [...prev, { path, content: data.data.content, originalContent: data.data.content, isModified: false }]); } } catch (e) { console.error(e); } } setActiveFile(path); }; const handleCloseFile = (path: string, e: React.MouseEvent) => { e.stopPropagation(); setOpenFiles(prev => { const next = prev.filter(f => f.path !== path); if (activeFile === path) { setActiveFile(next.length > 0 ? next[next.length - 1].path : null); } return next; }); }; const handleEditorChange = (val: string | undefined) => { if (!activeFile || val === undefined) return; setOpenFiles(prev => prev.map(f => { if (f.path === activeFile) { return { ...f, content: val, isModified: val !== f.originalContent }; } return f; })); }; const modifiedFiles = openFiles.filter(f => f.isModified); const handleCommit = async () => { if (modifiedFiles.length === 0 || !commitMessage.trim()) return; setCommitting(true); try { const filesRecord: Record = {}; modifiedFiles.forEach(f => filesRecord[f.path] = f.content); const res = await fetch(`/api/repos/${owner}/${repo}/ide/fs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ branch: defaultBranch, message: commitMessage, files: filesRecord }) }); if (res.ok) { setOpenFiles(prev => prev.map(f => ({ ...f, originalContent: f.content, isModified: false }))); setCommitMessage(""); alert("Committed successfully!"); } else { const data = await res.json(); alert("Failed to commit: " + data.message); } } catch (e) { console.error(e); alert("Error committing"); } finally { setCommitting(false); } }; const toggleDir = (node: TreeNode, e: React.MouseEvent) => { e.stopPropagation(); const toggleNode = (nodes: TreeNode[]): TreeNode[] => { return nodes.map(n => { if (n.path === node.path) return { ...n, isOpen: !n.isOpen }; if (n.children) return { ...n, children: toggleNode(n.children) }; return n; }); }; setTree(toggleNode(tree)); }; const renderTree = (nodes: TreeNode[], depth = 0) => { return nodes.map(node => (
node.type === "file" ? handleOpenFile(node.path) : toggleDir(node, e)} > {node.type === "dir" ? (
{node.isOpen ? : }
) : (
)} {node.type === "dir" ? ( ) : ( )} {node.name}
{node.type === "dir" && node.isOpen && node.children && renderTree(node.children, depth + 1)}
)); }; const activeFileData = openFiles.find(f => f.path === activeFile); const getLanguage = (filename: string) => { const ext = filename.split('.').pop()?.toLowerCase(); switch(ext) { case 'ts': case 'tsx': return 'typescript'; case 'js': case 'jsx': return 'javascript'; case 'json': return 'json'; case 'css': return 'css'; case 'html': return 'html'; case 'md': return 'markdown'; case 'astro': return 'html'; case 'py': return 'python'; case 'go': return 'go'; case 'rs': return 'rust'; case 'yml': case 'yaml': return 'yaml'; case 'sh': return 'shell'; case 'sql': return 'sql'; default: return 'plaintext'; } }; return (
{/* VS Code Title Bar */}
logo File Edit Selection View Go Run Terminal Help
{activeFileData ? `${activeFileData.path.split("/").pop()} - ` : ""}{repo} - OpenCodeHub
{/* Activity Bar */}
{/* Sidebar */} {isSidebarOpen && (
{activeTab === "explorer" && "Explorer"} {activeTab === "search" && "Search"} {activeTab === "scm" && "Source Control"}
{activeTab === "explorer" && (
{repo}
{loading ? (
Loading...
) : ( renderTree(tree) )}
)} {activeTab === "search" && (
Search is not available in the web editor.
)} {activeTab === "scm" && (