working frontend from @ishikabhoyar
This commit is contained in:
1
Frontend/.env
Normal file
1
Frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
SERVER_URL="http://localhost:8000"
|
||||
24
Frontend/.gitignore
vendored
Normal file
24
Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
13
Frontend/README.md
Normal file
13
Frontend/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# VSCode Clone with React and Vite
|
||||
|
||||
This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code.
|
||||
|
||||
## Features
|
||||
|
||||
- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc.
|
||||
- **Sidebar**: Displays file explorer, search results, and source control information.
|
||||
- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
|
||||
- **Panel**: Terminal, Problems, and Output views.
|
||||
- **Status Bar**: Displays status information and provides quick actions.
|
||||
|
||||
## Project Structure
|
||||
33
Frontend/eslint.config.js
Normal file
33
Frontend/eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
12
Frontend/index.html
Normal file
12
Frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VSCode</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3066
Frontend/package-lock.json
generated
Normal file
3066
Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Frontend/package.json
Normal file
31
Frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
1
Frontend/public/vite.svg
Normal file
1
Frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
13
Frontend/src/App.jsx
Normal file
13
Frontend/src/App.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import VSCodeUI from "./components/VSCodeUi"
|
||||
import "./index.css"
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<VSCodeUI />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
1
Frontend/src/assets/react.svg
Normal file
1
Frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
37
Frontend/src/components/ActivityBar.jsx
Normal file
37
Frontend/src/components/ActivityBar.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { Files, Search, GitBranch, Bug, Package, User, Settings } from "lucide-react";
|
||||
|
||||
const ActivityBar = ({ activeTab, toggleSidebar }) => {
|
||||
const tabs = [
|
||||
{ id: "explorer", icon: Files, title: "Explorer" },
|
||||
{ id: "search", icon: Search, title: "Search" },
|
||||
{ id: "git", icon: GitBranch, title: "Source Control" },
|
||||
{ id: "debug", icon: Bug, title: "Run and Debug" },
|
||||
{ id: "extensions", icon: Package, title: "Extensions" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="activity-bar">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={activeTab === tab.id ? "active" : ""}
|
||||
onClick={() => toggleSidebar(tab.id)}
|
||||
title={tab.title}
|
||||
>
|
||||
<tab.icon size={24} />
|
||||
</button>
|
||||
))}
|
||||
<div style={{ marginTop: "auto" }}>
|
||||
<button title="Accounts">
|
||||
<User size={24} />
|
||||
</button>
|
||||
<button title="Settings">
|
||||
<Settings size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityBar;
|
||||
890
Frontend/src/components/EditorArea.jsx
Normal file
890
Frontend/src/components/EditorArea.jsx
Normal file
@@ -0,0 +1,890 @@
|
||||
import React from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import {
|
||||
X, Plus, Save, FileCode, FileText, Folder, ChevronDown, ChevronRight,
|
||||
File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play,
|
||||
Terminal, Loader
|
||||
} from "lucide-react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Panel from "./Panel"; // Import Panel component
|
||||
|
||||
const EditorArea = ({
|
||||
sidebarVisible = true,
|
||||
activeView = "explorer",
|
||||
panelVisible,
|
||||
setPanelVisible
|
||||
}) => {
|
||||
// Store files with their content in state - start with just README.md
|
||||
const [files, setFiles] = useState([
|
||||
{ id: "README.md", language: "markdown", content: getDefaultCode("README.md") },
|
||||
]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(files[0]?.id || "");
|
||||
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState("");
|
||||
const [newFileType, setNewFileType] = useState("javascript");
|
||||
const [unsavedChanges, setUnsavedChanges] = useState({});
|
||||
|
||||
// Sidebar state - now receives visibility from props
|
||||
const [sidebarWidth, setSidebarWidth] = useState(250);
|
||||
const [expandedFolders, setExpandedFolders] = useState({});
|
||||
const [fileStructure, setFileStructure] = useState({
|
||||
'src': {
|
||||
type: 'folder',
|
||||
children: {}
|
||||
},
|
||||
'README.md': {
|
||||
type: 'file',
|
||||
language: 'markdown',
|
||||
id: 'README.md'
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu state
|
||||
const [showContextMenu, setShowContextMenu] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const [contextMenuTarget, setContextMenuTarget] = useState(null);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
const newFileInputRef = useRef(null);
|
||||
const renameInputRef = useRef(null);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [renamePath, setRenamePath] = useState('');
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
||||
// Replace terminal states with panel states
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [panelHeight, setPanelHeight] = useState(200);
|
||||
const [terminalOutput, setTerminalOutput] = useState([]);
|
||||
const [activeRunningFile, setActiveRunningFile] = useState(null);
|
||||
|
||||
// Focus the input when new file modal opens
|
||||
useEffect(() => {
|
||||
if (isNewFileModalOpen && newFileInputRef.current) {
|
||||
newFileInputRef.current.focus();
|
||||
}
|
||||
}, [isNewFileModalOpen]);
|
||||
|
||||
// Focus rename input when renaming
|
||||
useEffect(() => {
|
||||
if (isRenaming && renameInputRef.current) {
|
||||
renameInputRef.current.focus();
|
||||
}
|
||||
}, [isRenaming]);
|
||||
|
||||
// Load files and file structure from localStorage on component mount
|
||||
useEffect(() => {
|
||||
const savedFiles = localStorage.getItem("vscode-clone-files");
|
||||
const savedFileStructure = localStorage.getItem("vscode-clone-structure");
|
||||
|
||||
if (savedFiles) {
|
||||
try {
|
||||
const parsedFiles = JSON.parse(savedFiles);
|
||||
setFiles(parsedFiles);
|
||||
if (parsedFiles.length > 0) {
|
||||
setActiveTab(parsedFiles[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load saved files:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedFileStructure) {
|
||||
try {
|
||||
const parsedStructure = JSON.parse(savedFileStructure);
|
||||
setFileStructure(parsedStructure);
|
||||
} catch (error) {
|
||||
console.error("Failed to load file structure:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save files and file structure to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
||||
}, [files]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
||||
}, [fileStructure]);
|
||||
|
||||
// Add this effect to handle editor resize when sidebar changes
|
||||
useEffect(() => {
|
||||
// Force editor to readjust layout when sidebar visibility changes
|
||||
if (editorRef.current) {
|
||||
setTimeout(() => {
|
||||
editorRef.current.layout();
|
||||
}, 300); // Small delay to allow transition to complete
|
||||
}
|
||||
}, [sidebarVisible]);
|
||||
|
||||
// Add this effect to sync the panel state with parent component
|
||||
useEffect(() => {
|
||||
if (panelVisible !== undefined) {
|
||||
setShowPanel(panelVisible);
|
||||
}
|
||||
}, [panelVisible]);
|
||||
|
||||
const handleEditorDidMount = (editor) => {
|
||||
editorRef.current = editor;
|
||||
};
|
||||
|
||||
const handleEditorChange = (value) => {
|
||||
// Mark the current file as having unsaved changes
|
||||
setUnsavedChanges(prev => ({
|
||||
...prev,
|
||||
[activeTab]: true
|
||||
}));
|
||||
|
||||
// Update the file content in the files array
|
||||
setFiles(files.map(file =>
|
||||
file.id === activeTab ? { ...file, content: value } : file
|
||||
));
|
||||
};
|
||||
|
||||
const handleCloseTab = (e, fileId) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (unsavedChanges[fileId]) {
|
||||
if (!confirm(`You have unsaved changes in ${fileId}. Close anyway?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the file from the files array
|
||||
const newFiles = files.filter(file => file.id !== fileId);
|
||||
setFiles(newFiles);
|
||||
|
||||
// Update unsavedChanges
|
||||
const newUnsavedChanges = { ...unsavedChanges };
|
||||
delete newUnsavedChanges[fileId];
|
||||
setUnsavedChanges(newUnsavedChanges);
|
||||
|
||||
// If the active tab is closed, set a new active tab
|
||||
if (activeTab === fileId && newFiles.length > 0) {
|
||||
setActiveTab(newFiles[0].id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNewFile = (e, path = '') => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (!newFileName) return;
|
||||
|
||||
const filePath = path ? `${path}/${newFileName}` : newFileName;
|
||||
|
||||
// Check if file already exists
|
||||
if (files.some(file => file.id === filePath)) {
|
||||
alert(`A file named "${filePath}" already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine language based on file extension
|
||||
let language = newFileType;
|
||||
const extension = newFileName.split('.').pop().toLowerCase();
|
||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||
language = 'javascript';
|
||||
} else if (['css', 'scss', 'less'].includes(extension)) {
|
||||
language = 'css';
|
||||
} else if (['html', 'htm'].includes(extension)) {
|
||||
language = 'html';
|
||||
} else if (['json'].includes(extension)) {
|
||||
language = 'json';
|
||||
} else if (['md', 'markdown'].includes(extension)) {
|
||||
language = 'markdown';
|
||||
}
|
||||
|
||||
// Create new file
|
||||
const newFile = {
|
||||
id: filePath,
|
||||
language,
|
||||
content: ''
|
||||
};
|
||||
|
||||
setFiles([...files, newFile]);
|
||||
setActiveTab(filePath);
|
||||
|
||||
// Update file structure
|
||||
updateFileStructure(filePath, 'file', language);
|
||||
|
||||
setNewFileName('');
|
||||
setIsNewFileModalOpen(false);
|
||||
};
|
||||
|
||||
const updateFileStructure = (path, type, language = null) => {
|
||||
const parts = path.split('/');
|
||||
const fileName = parts.pop();
|
||||
let current = fileStructure;
|
||||
|
||||
// Navigate to the correct folder
|
||||
if (parts.length > 0) {
|
||||
for (const part of parts) {
|
||||
if (!current[part]) {
|
||||
current[part] = { type: 'folder', children: {} };
|
||||
}
|
||||
current = current[part].children;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new item to the structure
|
||||
if (type === 'file') {
|
||||
current[fileName] = { type: 'file', language, id: path };
|
||||
} else if (type === 'folder') {
|
||||
current[fileName] = { type: 'folder', children: {} };
|
||||
}
|
||||
|
||||
// Update the state with the new structure
|
||||
setFileStructure({...fileStructure});
|
||||
};
|
||||
|
||||
const createNewFolder = (path = '') => {
|
||||
const folderName = prompt("Enter folder name:");
|
||||
if (!folderName) return;
|
||||
|
||||
const folderPath = path ? `${path}/${folderName}` : folderName;
|
||||
updateFileStructure(folderPath, 'folder');
|
||||
|
||||
// If the folder is inside another folder, expand the parent
|
||||
if (path) {
|
||||
setExpandedFolders({
|
||||
...expandedFolders,
|
||||
[path]: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFile = () => {
|
||||
// Mark current file as saved
|
||||
setUnsavedChanges(prev => ({
|
||||
...prev,
|
||||
[activeTab]: false
|
||||
}));
|
||||
|
||||
// In a real app, you would save to the server here
|
||||
console.log(`File ${activeTab} saved!`);
|
||||
};
|
||||
|
||||
const toggleFolder = (folderPath) => {
|
||||
setExpandedFolders({
|
||||
...expandedFolders,
|
||||
[folderPath]: !expandedFolders[folderPath]
|
||||
});
|
||||
};
|
||||
|
||||
const openFile = (fileId) => {
|
||||
// Check if file exists in files array
|
||||
const fileExists = files.some(file => file.id === fileId);
|
||||
|
||||
if (!fileExists) {
|
||||
// Determine language from file structure
|
||||
let language = 'text';
|
||||
const parts = fileId.split('/');
|
||||
const fileName = parts.pop();
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||
language = 'javascript';
|
||||
} else if (['css', 'scss', 'less'].includes(extension)) {
|
||||
language = 'css';
|
||||
} else if (['html', 'htm'].includes(extension)) {
|
||||
language = 'html';
|
||||
} else if (['json'].includes(extension)) {
|
||||
language = 'json';
|
||||
} else if (['md', 'markdown'].includes(extension)) {
|
||||
language = 'markdown';
|
||||
}
|
||||
|
||||
// Create new file entry
|
||||
const newFile = {
|
||||
id: fileId,
|
||||
language,
|
||||
content: ''
|
||||
};
|
||||
|
||||
setFiles([...files, newFile]);
|
||||
}
|
||||
|
||||
setActiveTab(fileId);
|
||||
};
|
||||
|
||||
const handleContextMenu = (e, path, type) => {
|
||||
e.preventDefault();
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuTarget({ path, type });
|
||||
setShowContextMenu(true);
|
||||
};
|
||||
|
||||
const closeContextMenu = () => {
|
||||
setShowContextMenu(false);
|
||||
setContextMenuTarget(null);
|
||||
};
|
||||
|
||||
const deleteItem = (path, type) => {
|
||||
const confirmDelete = confirm(`Are you sure you want to delete ${path}?`);
|
||||
if (!confirmDelete) return;
|
||||
|
||||
if (type === 'file') {
|
||||
// Remove from files array
|
||||
setFiles(files.filter(file => file.id !== path));
|
||||
|
||||
// If it was active, set a new active tab
|
||||
if (activeTab === path) {
|
||||
const newActiveTab = files.find(file => file.id !== path)?.id || '';
|
||||
setActiveTab(newActiveTab);
|
||||
}
|
||||
|
||||
// Remove from unsavedChanges
|
||||
const newUnsavedChanges = { ...unsavedChanges };
|
||||
delete newUnsavedChanges[path];
|
||||
setUnsavedChanges(newUnsavedChanges);
|
||||
}
|
||||
|
||||
// Remove from file structure
|
||||
const parts = path.split('/');
|
||||
const itemName = parts.pop();
|
||||
let current = fileStructure;
|
||||
let parent = null;
|
||||
|
||||
// Navigate to the correct folder
|
||||
if (parts.length > 0) {
|
||||
for (const part of parts) {
|
||||
parent = current;
|
||||
current = current[part].children;
|
||||
}
|
||||
|
||||
// Delete the item
|
||||
delete current[itemName];
|
||||
} else {
|
||||
// Delete top-level item
|
||||
delete fileStructure[itemName];
|
||||
}
|
||||
|
||||
// Update the state
|
||||
setFileStructure({...fileStructure});
|
||||
};
|
||||
|
||||
const startRenaming = (path, type) => {
|
||||
setRenamePath(path);
|
||||
|
||||
const parts = path.split('/');
|
||||
const currentName = parts.pop();
|
||||
setRenameValue(currentName);
|
||||
|
||||
setIsRenaming(true);
|
||||
};
|
||||
|
||||
const handleRename = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!renameValue.trim()) return;
|
||||
|
||||
const parts = renamePath.split('/');
|
||||
const oldName = parts.pop();
|
||||
const parentPath = parts.join('/');
|
||||
const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue;
|
||||
|
||||
// Check if this would overwrite an existing file or folder
|
||||
const parts2 = newPath.split('/');
|
||||
const newName = parts2.pop();
|
||||
let current = fileStructure;
|
||||
|
||||
// Navigate to parent folder
|
||||
for (let i = 0; i < parts2.length; i++) {
|
||||
current = current[parts2[i]].children;
|
||||
}
|
||||
|
||||
if (current[newName] && renamePath !== newPath) {
|
||||
alert(`An item named "${newName}" already exists at this location.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the object data
|
||||
const pathParts = renamePath.split('/');
|
||||
let curr = fileStructure;
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
curr = curr[pathParts[i]].children;
|
||||
}
|
||||
const item = curr[pathParts[pathParts.length - 1]];
|
||||
|
||||
// Delete from old location
|
||||
delete curr[pathParts[pathParts.length - 1]];
|
||||
|
||||
// Add to new location
|
||||
const newParts = newPath.split('/');
|
||||
curr = fileStructure;
|
||||
for (let i = 0; i < newParts.length - 1; i++) {
|
||||
curr = curr[newParts[i]].children;
|
||||
}
|
||||
curr[newParts[newParts.length - 1]] = item;
|
||||
|
||||
// If it's a file, update the files array
|
||||
if (item.type === 'file') {
|
||||
const fileIndex = files.findIndex(file => file.id === renamePath);
|
||||
if (fileIndex !== -1) {
|
||||
const updatedFiles = [...files];
|
||||
updatedFiles[fileIndex] = {
|
||||
...updatedFiles[fileIndex],
|
||||
id: newPath
|
||||
};
|
||||
setFiles(updatedFiles);
|
||||
|
||||
// Update active tab if necessary
|
||||
if (activeTab === renamePath) {
|
||||
setActiveTab(newPath);
|
||||
}
|
||||
|
||||
// Update unsavedChanges
|
||||
if (unsavedChanges[renamePath]) {
|
||||
const newUnsavedChanges = { ...unsavedChanges };
|
||||
newUnsavedChanges[newPath] = newUnsavedChanges[renamePath];
|
||||
delete newUnsavedChanges[renamePath];
|
||||
setUnsavedChanges(newUnsavedChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFileStructure({...fileStructure});
|
||||
setIsRenaming(false);
|
||||
};
|
||||
|
||||
const cancelRename = () => {
|
||||
setIsRenaming(false);
|
||||
setRenamePath('');
|
||||
setRenameValue('');
|
||||
};
|
||||
|
||||
const getFileIcon = (fileName) => {
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||
return <FileCode size={14} className="mr-1 text-yellow-400" />;
|
||||
} else if (['css', 'scss', 'less'].includes(extension)) {
|
||||
return <FileCode size={14} className="mr-1 text-blue-400" />;
|
||||
} else if (['html', 'htm'].includes(extension)) {
|
||||
return <FileCode size={14} className="mr-1 text-orange-400" />;
|
||||
} else if (['md', 'markdown'].includes(extension)) {
|
||||
return <FileText size={14} className="mr-1 text-white" />;
|
||||
}
|
||||
return <FileText size={14} className="mr-1" />;
|
||||
};
|
||||
|
||||
function getDefaultCode(tabId) {
|
||||
switch (tabId) {
|
||||
case "README.md":
|
||||
return `# VS Code Clone Project
|
||||
|
||||
## Overview
|
||||
This is a simple VS Code clone built with React and Monaco Editor.
|
||||
|
||||
## Features
|
||||
- File tree navigation
|
||||
- Tab management
|
||||
- Code editing with Monaco Editor
|
||||
- Syntax highlighting
|
||||
|
||||
## Getting Started
|
||||
1. Create a new file using the + button in the sidebar
|
||||
2. Edit your code in the editor
|
||||
3. Save changes using the save button
|
||||
|
||||
Happy coding!`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const activeFile = files.find(file => file.id === activeTab);
|
||||
|
||||
// Calculate editor area style based on sidebar visibility
|
||||
const editorAreaStyle = {
|
||||
marginLeft: sidebarVisible ? `${sidebarWidth}px` : '0px',
|
||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||
};
|
||||
|
||||
// Update the run code function to work with backend
|
||||
const handleRunCode = async () => {
|
||||
if (!activeFile) return;
|
||||
|
||||
// Show the panel
|
||||
setShowPanel(true);
|
||||
if (setPanelVisible) {
|
||||
setPanelVisible(true);
|
||||
}
|
||||
|
||||
// Set running state
|
||||
setIsRunning(true);
|
||||
setActiveRunningFile(activeFile.id);
|
||||
|
||||
// Clear previous output and add new command
|
||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||
const language = getLanguageFromExtension(fileExtension);
|
||||
|
||||
const newOutput = [
|
||||
{ type: 'command', content: `$ run ${activeFile.id}` }
|
||||
];
|
||||
setTerminalOutput(newOutput);
|
||||
|
||||
try {
|
||||
// Step 1: Submit code to backend
|
||||
const submitResponse = await fetch('http://localhost:8080/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: language,
|
||||
code: activeFile.content
|
||||
}),
|
||||
});
|
||||
|
||||
if (!submitResponse.ok) {
|
||||
throw new Error(`Server error: ${submitResponse.status}`);
|
||||
}
|
||||
|
||||
const { id } = await submitResponse.json();
|
||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||
|
||||
// Step 2: Poll for status until completed or failed
|
||||
let status = 'pending';
|
||||
while (status !== 'completed' && status !== 'failed') {
|
||||
// Add a small delay between polls
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const statusResponse = await fetch(`http://localhost:8080/status?id=${id}`);
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
||||
}
|
||||
|
||||
const statusData = await statusResponse.json();
|
||||
status = statusData.status;
|
||||
|
||||
// Update terminal with status (for any status type)
|
||||
setTerminalOutput(prev => {
|
||||
// Update the last status message or add a new one
|
||||
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
||||
if (hasStatus) {
|
||||
return prev.map(line =>
|
||||
line.content.includes('Status:')
|
||||
? { ...line, content: `Status: ${status}` }
|
||||
: line
|
||||
);
|
||||
} else {
|
||||
return [...prev, { type: 'output', content: `Status: ${status}` }];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the result for both completed and failed status
|
||||
const resultResponse = await fetch(`http://localhost:8080/result?id=${id}`);
|
||||
if (!resultResponse.ok) {
|
||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
||||
}
|
||||
|
||||
const { output } = await resultResponse.json();
|
||||
|
||||
// Format and display output
|
||||
const outputLines = output.split('\n').map(line => ({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: line
|
||||
}));
|
||||
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
{
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: status === 'failed'
|
||||
? '------- EXECUTION FAILED -------'
|
||||
: '------- EXECUTION RESULT -------'
|
||||
},
|
||||
...outputLines
|
||||
]);
|
||||
|
||||
if (status === 'failed') {
|
||||
console.error('Code execution failed:', output);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
} finally {
|
||||
// Set running state to false
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to convert file extension to language identifier for API
|
||||
const getLanguageFromExtension = (extension) => {
|
||||
const languageMap = {
|
||||
'java': 'java',
|
||||
'c': 'c',
|
||||
'cpp': 'cpp',
|
||||
'py': 'python',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript'
|
||||
};
|
||||
|
||||
return languageMap[extension] || extension;
|
||||
};
|
||||
|
||||
// Update this function to also update parent state
|
||||
const togglePanel = () => {
|
||||
const newState = !showPanel;
|
||||
setShowPanel(newState);
|
||||
if (setPanelVisible) {
|
||||
setPanelVisible(newState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="editor-container">
|
||||
{sidebarVisible && (
|
||||
<Sidebar
|
||||
activeView={activeView}
|
||||
width={sidebarWidth}
|
||||
fileStructure={fileStructure}
|
||||
expandedFolders={expandedFolders}
|
||||
setExpandedFolders={setExpandedFolders}
|
||||
handleContextMenu={handleContextMenu}
|
||||
openFile={openFile}
|
||||
unsavedChanges={unsavedChanges}
|
||||
activeTab={activeTab}
|
||||
isRenaming={isRenaming}
|
||||
renamePath={renamePath}
|
||||
renameValue={renameValue}
|
||||
handleRename={handleRename}
|
||||
renameInputRef={renameInputRef}
|
||||
cancelRename={cancelRename}
|
||||
setIsNewFileModalOpen={setIsNewFileModalOpen}
|
||||
createNewFolder={createNewFolder}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="editor-area" style={editorAreaStyle}>
|
||||
<div className="editor-header">
|
||||
<div className="editor-tabs">
|
||||
{files.map((file) => {
|
||||
// Extract just the filename without path for display
|
||||
const displayName = file.id.includes('/') ?
|
||||
file.id.split('/').pop() :
|
||||
file.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className={`editor-tab ${activeTab === file.id ? "active" : ""}`}
|
||||
onClick={() => setActiveTab(file.id)}
|
||||
title={file.id} // Show full path on hover
|
||||
>
|
||||
<span className="tab-name">
|
||||
{getFileIcon(file.id)}
|
||||
{displayName} {/* Show just filename, not full path */}
|
||||
{unsavedChanges[file.id] && ' •'}
|
||||
</span>
|
||||
<button
|
||||
className="tab-close"
|
||||
onClick={(e) => handleCloseTab(e, file.id)}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
className="editor-tab-new"
|
||||
onClick={() => setIsNewFileModalOpen(true)}
|
||||
title="Create new file"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Run controls */}
|
||||
<div className="editor-run-controls">
|
||||
{activeFile && (
|
||||
<>
|
||||
<button
|
||||
className="run-button"
|
||||
onClick={handleRunCode}
|
||||
disabled={isRunning}
|
||||
title="Run code"
|
||||
>
|
||||
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
|
||||
<span>Run</span>
|
||||
</button>
|
||||
<button
|
||||
className="terminal-toggle-button"
|
||||
onClick={togglePanel} // Use the new function
|
||||
title="Toggle terminal"
|
||||
>
|
||||
<Terminal size={16} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="monaco-container" style={{
|
||||
height: showPanel ? `calc(100% - ${panelHeight}px - 30px)` : "100%"
|
||||
}}>
|
||||
{activeFile ? (
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage={activeFile.language}
|
||||
language={activeFile.language}
|
||||
value={activeFile.content}
|
||||
theme="vs-dark"
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={handleEditorChange}
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
lineNumbers: "on",
|
||||
renderLineHighlight: "all",
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="empty-editor-message">
|
||||
<p>No file open. Create a new file or select a file to start editing.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Use Panel component instead of internal terminal */}
|
||||
{showPanel && (
|
||||
<>
|
||||
<div
|
||||
className="resize-handle resize-handle-horizontal"
|
||||
onMouseDown={(e) => {
|
||||
const startY = e.clientY;
|
||||
const startHeight = panelHeight;
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const newHeight = startHeight - (moveEvent.clientY - startY);
|
||||
if (newHeight > 100 && newHeight < 500) {
|
||||
setPanelHeight(newHeight);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
/>
|
||||
<Panel
|
||||
height={panelHeight}
|
||||
terminalOutput={terminalOutput}
|
||||
isRunning={isRunning}
|
||||
activeRunningFile={activeRunningFile}
|
||||
initialTab="terminal"
|
||||
onClose={togglePanel} // Use the new function
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="editor-actions">
|
||||
<button
|
||||
className="editor-action-button"
|
||||
onClick={handleSaveFile}
|
||||
disabled={!activeTab || !unsavedChanges[activeTab]}
|
||||
title="Save file"
|
||||
>
|
||||
<Save size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isNewFileModalOpen && (
|
||||
<div className="modal-overlay">
|
||||
<div className="new-file-modal">
|
||||
<h3>Create New File</h3>
|
||||
<form onSubmit={handleCreateNewFile}>
|
||||
<input
|
||||
ref={newFileInputRef}
|
||||
type="text"
|
||||
placeholder="File name (e.g., NewFile.jsx)"
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
/>
|
||||
<div className="modal-actions">
|
||||
<button type="button" onClick={() => setIsNewFileModalOpen(false)}>Cancel</button>
|
||||
<button type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context Menu */}
|
||||
{showContextMenu && (
|
||||
<div
|
||||
className="context-menu"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: contextMenuPosition.y,
|
||||
left: contextMenuPosition.x
|
||||
}}
|
||||
>
|
||||
{contextMenuTarget?.type === 'folder' && (
|
||||
<>
|
||||
<div className="context-menu-item" onClick={() => {
|
||||
createNewFolder(contextMenuTarget.path);
|
||||
closeContextMenu();
|
||||
}}>
|
||||
<FolderPlus size={14} className="mr-1" />
|
||||
New Folder
|
||||
</div>
|
||||
<div className="context-menu-item" onClick={() => {
|
||||
setNewFileName('');
|
||||
setIsNewFileModalOpen(true);
|
||||
closeContextMenu();
|
||||
}}>
|
||||
<FilePlus size={14} className="mr-1" />
|
||||
New File
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="context-menu-item" onClick={() => {
|
||||
startRenaming(contextMenuTarget.path, contextMenuTarget.type);
|
||||
closeContextMenu();
|
||||
}}>
|
||||
<Edit size={14} className="mr-1" />
|
||||
Rename
|
||||
</div>
|
||||
<div className="context-menu-item delete" onClick={() => {
|
||||
deleteItem(contextMenuTarget.path, contextMenuTarget.type);
|
||||
closeContextMenu();
|
||||
}}>
|
||||
<Trash2 size={14} className="mr-1" />
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showContextMenu && (
|
||||
<div
|
||||
className="context-menu-overlay"
|
||||
onClick={closeContextMenu}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorArea;
|
||||
|
||||
150
Frontend/src/components/Navbar.jsx
Normal file
150
Frontend/src/components/Navbar.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from "react"
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
|
||||
export function Navbar() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<span className="text-xl font-bold">*Azzle</span>
|
||||
</Link>
|
||||
|
||||
<NavigationMenu className="hidden md:flex">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Demo <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/demo/features"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Features</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Explore all the features our platform has to offer
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/demo/pricing"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Pricing</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
View our flexible pricing plans
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/about" legacyBehavior passHref>
|
||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
||||
About
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Services <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/services/consulting"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Consulting</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Expert guidance for your business needs
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/services/implementation"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Implementation</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Full-service implementation and support
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Pages <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Blog</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Read our latest articles and updates
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/resources"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Resources</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Helpful guides and documentation
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/contact" legacyBehavior passHref>
|
||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
||||
Contact
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/signup">Sign up free</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
173
Frontend/src/components/Panel.jsx
Normal file
173
Frontend/src/components/Panel.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const Panel = ({
|
||||
height,
|
||||
terminalOutput = [],
|
||||
isRunning = false,
|
||||
activeRunningFile = null,
|
||||
initialTab = "terminal",
|
||||
onClose
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
|
||||
// Set active tab when initialTab changes
|
||||
useEffect(() => {
|
||||
setActiveTab(initialTab);
|
||||
}, [initialTab]);
|
||||
|
||||
const renderTerminal = () => {
|
||||
return (
|
||||
<div className="panel-terminal">
|
||||
{terminalOutput.length > 0 ? (
|
||||
// Render output from EditorArea when available
|
||||
<>
|
||||
{terminalOutput.map((line, index) => (
|
||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
|
||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
|
||||
</div>
|
||||
))}
|
||||
{isRunning && (
|
||||
<div className="terminal-line">
|
||||
<span className="terminal-cursor"></span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Default terminal content when no output
|
||||
<>
|
||||
<div className="terminal-line">
|
||||
<span className="terminal-prompt">$</span> npm start
|
||||
</div>
|
||||
<div className="terminal-line terminal-output">Starting the development server...</div>
|
||||
<div className="terminal-line terminal-output">Compiled successfully!</div>
|
||||
<div className="terminal-line terminal-output">You can now view vscode-clone in the browser.</div>
|
||||
<div className="terminal-line terminal-output">Local: http://localhost:3000</div>
|
||||
<div className="terminal-line terminal-output">On Your Network: http://192.168.1.5:3000</div>
|
||||
<div className="terminal-line">
|
||||
<span className="terminal-prompt">$</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderProblems = () => {
|
||||
return (
|
||||
<div className="panel-problems">
|
||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutput = () => {
|
||||
return (
|
||||
<div className="panel-output">
|
||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||
<div className="output-line">[Language Server] Language server started.</div>
|
||||
{activeRunningFile && (
|
||||
<div className="output-line">[Running] {activeRunningFile}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case "terminal":
|
||||
return renderTerminal();
|
||||
case "problems":
|
||||
return renderProblems();
|
||||
case "output":
|
||||
return renderOutput();
|
||||
default:
|
||||
return <div>Unknown tab</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel" style={{ height: `${height}px` }}>
|
||||
<div className="panel-tabs">
|
||||
<div
|
||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("problems")}
|
||||
>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Problems</span>
|
||||
</div>
|
||||
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Output</span>
|
||||
</div>
|
||||
<div
|
||||
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("terminal")}
|
||||
>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Terminal</span>
|
||||
</div>
|
||||
|
||||
{/* Add close button */}
|
||||
<div className="panel-actions">
|
||||
<button className="panel-close-btn" onClick={onClose}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-content">{getTabContent()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Panel;
|
||||
|
||||
321
Frontend/src/components/Sidebar.jsx
Normal file
321
Frontend/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const Sidebar = ({
|
||||
activeView,
|
||||
width,
|
||||
fileStructure,
|
||||
expandedFolders,
|
||||
setExpandedFolders,
|
||||
handleContextMenu,
|
||||
openFile,
|
||||
unsavedChanges,
|
||||
activeTab,
|
||||
isRenaming,
|
||||
renamePath,
|
||||
renameValue,
|
||||
handleRename,
|
||||
renameInputRef,
|
||||
cancelRename,
|
||||
setIsNewFileModalOpen, // Add this prop
|
||||
createNewFolder
|
||||
}) => {
|
||||
|
||||
const toggleFolder = (folderPath) => {
|
||||
setExpandedFolders((prev) => ({
|
||||
...prev,
|
||||
[folderPath]: !prev[folderPath],
|
||||
}));
|
||||
};
|
||||
|
||||
const renderExplorer = () => {
|
||||
const renderFileTree = (structure, path = "") => {
|
||||
if (!structure) return null;
|
||||
|
||||
return Object.entries(structure).map(([name, item]) => {
|
||||
const currentPath = path ? `${path}/${name}` : name;
|
||||
|
||||
if (item.type === "folder") {
|
||||
const isExpanded = expandedFolders[currentPath];
|
||||
return (
|
||||
<div key={currentPath} className="file-tree-item">
|
||||
<div
|
||||
className="file-tree-folder"
|
||||
onClick={() => toggleFolder(currentPath)}
|
||||
onContextMenu={(e) => handleContextMenu(e, currentPath, 'folder')}
|
||||
>
|
||||
<span className="folder-icon">
|
||||
{isExpanded ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="folder-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#75beff"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="folder-name">{name}</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="file-tree-children">
|
||||
{renderFileTree(item.children, currentPath)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// It's a file
|
||||
const isActive = activeTab === currentPath;
|
||||
return (
|
||||
<div key={currentPath} className="file-tree-item">
|
||||
<div
|
||||
className={`file-tree-file ${isActive ? 'active' : ''}`}
|
||||
onClick={() => openFile(currentPath)}
|
||||
onContextMenu={(e) => handleContextMenu(e, currentPath, 'file')}
|
||||
>
|
||||
<span className="file-icon">
|
||||
{getFileIcon(name)}
|
||||
</span>
|
||||
<span className="file-name">
|
||||
{isRenaming && renamePath === currentPath ? (
|
||||
<form onSubmit={handleRename}>
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
type="text"
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onBlur={cancelRename}
|
||||
className="rename-input"
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
{name}
|
||||
{unsavedChanges[currentPath] && <span className="unsaved-indicator"> •</span>}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-title">
|
||||
<span>EXPLORER</span>
|
||||
<div className="sidebar-actions">
|
||||
<button className="sidebar-action" onClick={() => createNewFolder()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
<line x1="12" y1="11" x2="12" y2="17"></line>
|
||||
<line x1="9" y1="14" x2="15" y2="14"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="sidebar-action" onClick={() => setIsNewFileModalOpen(true)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="12" y1="12" x2="12" y2="18"></line>
|
||||
<line x1="9" y1="15" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-tree">{renderFileTree(fileStructure)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFileIcon = (fileName) => {
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#e6db74"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
);
|
||||
} else if (['css', 'scss', 'less'].includes(extension)) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#66d9ef"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
);
|
||||
} else if (['md', 'markdown'].includes(extension)) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSearch = () => {
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-title">SEARCH</div>
|
||||
<div className="sidebar-search">
|
||||
<div className="search-input-container">
|
||||
<input type="text" placeholder="Search" className="search-input" />
|
||||
<span className="search-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGit = () => {
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-title">SOURCE CONTROL</div>
|
||||
<div className="sidebar-empty-message">No changes detected</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeView) {
|
||||
case "explorer":
|
||||
return renderExplorer();
|
||||
case "search":
|
||||
return renderSearch();
|
||||
case "git":
|
||||
return renderGit();
|
||||
default:
|
||||
return <div className="sidebar-empty-message">Content for {activeView}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar" style={{ width: `${width}px` }}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
105
Frontend/src/components/StatusBar.jsx
Normal file
105
Frontend/src/components/StatusBar.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
"use client"
|
||||
|
||||
const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
return (
|
||||
<div className="status-bar">
|
||||
<div className="status-bar-left">
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="6" y1="3" x2="6" y2="15"></line>
|
||||
<circle cx="18" cy="6" r="3"></circle>
|
||||
<circle cx="6" cy="18" r="3"></circle>
|
||||
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
||||
</svg>
|
||||
<span>main</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span>0 errors</span>
|
||||
</div>
|
||||
|
||||
<button className="status-item status-button" onClick={togglePanel}>
|
||||
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-right">
|
||||
<div className="status-item">
|
||||
<span>Ln 1, Col 1</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<span>Spaces: 2</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<span>UTF-8</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
||||
</svg>
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusBar
|
||||
|
||||
54
Frontend/src/components/VSCodeUI.jsx
Normal file
54
Frontend/src/components/VSCodeUI.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import ActivityBar from "./ActivityBar"
|
||||
import EditorArea from "./EditorArea"
|
||||
import StatusBar from "./StatusBar"
|
||||
|
||||
const VSCodeUI = () => {
|
||||
const [activeView, setActiveView] = useState("explorer")
|
||||
const [sidebarVisible, setSidebarVisible] = useState(true)
|
||||
const [panelVisible, setPanelVisible] = useState(true)
|
||||
const [panelHeight, setPanelHeight] = useState(200)
|
||||
|
||||
// Effect to handle resize when sidebar visibility changes
|
||||
useEffect(() => {
|
||||
// Force layout recalculation
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, [sidebarVisible]);
|
||||
|
||||
const toggleSidebar = (view) => {
|
||||
if (activeView === view && sidebarVisible) {
|
||||
setSidebarVisible(false)
|
||||
} else {
|
||||
setActiveView(view)
|
||||
setSidebarVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
const togglePanel = () => {
|
||||
setPanelVisible(!panelVisible)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vscode-container">
|
||||
<ActivityBar activeTab={activeView} toggleSidebar={toggleSidebar} />
|
||||
<div className="vscode-main">
|
||||
<div className="editor-container">
|
||||
<EditorArea
|
||||
sidebarVisible={sidebarVisible}
|
||||
activeView={activeView}
|
||||
panelVisible={panelVisible}
|
||||
setPanelVisible={setPanelVisible}
|
||||
/>
|
||||
|
||||
{/* Remove the duplicate Panel component from here */}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBar togglePanel={togglePanel} panelVisible={panelVisible} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VSCodeUI
|
||||
1006
Frontend/src/index.css
Normal file
1006
Frontend/src/index.css
Normal file
File diff suppressed because it is too large
Load Diff
11
Frontend/src/main.jsx
Normal file
11
Frontend/src/main.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { StrictMode } from 'react'
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
6
Frontend/vite.config.js
Normal file
6
Frontend/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user