From e2bb95e7d604a5c5f6bfe63c1510537b76d87a27 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:46:02 +0530 Subject: [PATCH 01/11] Refactor FileExplorer components to use Zustand for state management, enabling dynamic file and folder handling. Updated FileCom and FolderCom to utilize hooks for file data retrieval and interaction. Enhanced CodeEditor to synchronize language and content with active files. Added Zustand and UUID as dependencies. --- .../CodeEditor/[id]/FileExplorer/FileCom.tsx | 18 ++- .../[id]/FileExplorer/FileExplorer.tsx | 5 +- .../[id]/FileExplorer/FolderCom.tsx | 103 +++++++----------- remote-ide/app/CodeEditor/[id]/page.tsx | 61 ++++++++--- remote-ide/package-lock.json | 46 +++++++- remote-ide/package.json | 4 +- 6 files changed, 149 insertions(+), 88 deletions(-) diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx index e71e845..a4ee279 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx @@ -1,11 +1,19 @@ import { File } from "lucide-react"; -import { FileTree} from "./types"; +import { useFileStore } from "../../../../hooks/useFileStore"; + +export const FileCom = ({ fileId }: { fileId: string }) => { + const { files, setActiveFile } = useFileStore(); + const file = files.find((f) => f.id === fileId); + + if (!file) return null; -export const FileCom = ({ data }: { data: FileTree }) => { return ( -
- - {data.name} +
setActiveFile(fileId)} + > + + {file.name}
); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx index 97352b1..48122cd 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx @@ -1,6 +1,7 @@ +// FileExplorer.tsx + import { FilePlus, FolderPlus, Search } from "lucide-react"; import { FolderCom } from "./FolderCom"; -import { Folder_Example,FileTree } from "./types"; export const FileExplorer = () => { return ( @@ -15,7 +16,7 @@ export const FileExplorer = () => {
- + ); }; \ No newline at end of file diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index cd9e2ab..b0253bc 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -1,95 +1,66 @@ import { useState } from "react"; import { Folder, ChevronDown, ChevronRight, Plus } from "lucide-react"; -import { FileTree } from "./types"; +import { useFileStore } from "../../../../hooks/useFileStore"; import { FileCom } from "./FileCom"; -export const FolderCom = ({ data }: { data: FileTree }) => { +export const FolderCom = ({ folderId }: { folderId: string }) => { const [open, setOpen] = useState(true); const [showPopup, setShowPopup] = useState(false); - const [newItemName, setNewItemName] = useState(""); - const [newItemType, setNewItemType] = useState("File"); + const [name, setName] = useState(""); + const [type, setType] = useState<"file" | "folder">("file"); - const add_To_FileTree = () => { - setShowPopup(true); - }; + const { files, addFile, addFolder } = useFileStore(); - const handleAddItem = () => { - if (!newItemName.trim()) return; + const children = files.filter((f) => f.parentId === folderId); + const folder = files.find((f) => f.id === folderId); - const newItem: FileTree = { - id: Math.random().toString(36), - name: newItemName, - type: newItemType as "Folder" | "File", - children: [], - }; + if (!folder) return null; - data.children?.push(newItem); - setNewItemName(""); + const handleAdd = () => { + if (!name.trim()) return; + type === "file" ? addFile(name, folderId) : addFolder(name, folderId); + setName(""); setShowPopup(false); }; return (
setOpen(!open)} - onAuxClick={add_To_FileTree} - className="cursor-pointer hover:bg-blue-900/50 p-1 rounded flex gap-1 items-center transition" > - {open ? ( - - ) : ( - - )} - - {data.name} - : } + + {folder.name} + { + e.stopPropagation(); + setShowPopup(true); + }} />
+ {open && ( -
- {data.children?.map((child) => { - if (child.type === "Folder") { - return ; - } else { - return ; - } - })} +
+ {children.map((child) => + child.type === "folder" ? ( + + ) : ( + + ) + )}
)} {showPopup && ( -
- setNewItemName(e.target.value)} - className="p-1 rounded text-black" - /> - setName(e.target.value)} /> + -
- - -
+
)}
diff --git a/remote-ide/app/CodeEditor/[id]/page.tsx b/remote-ide/app/CodeEditor/[id]/page.tsx index 6696dcf..d81ab46 100644 --- a/remote-ide/app/CodeEditor/[id]/page.tsx +++ b/remote-ide/app/CodeEditor/[id]/page.tsx @@ -1,16 +1,18 @@ +// CodeEditor/page.tsx + "use client"; import { useState, useRef, useEffect } from "react"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import { - Terminal -} from "lucide-react"; +import { Terminal } from "lucide-react"; import Editor from "@monaco-editor/react"; import { Header } from "./Header"; import { CODE_SNIPPETS, FILE_NAMES, THEMES } from "./constants"; import { Button } from "@/components/ui/button"; import { FileExplorer } from "./FileExplorer/FileExplorer"; +import { useFileStore } from "@/hooks/useFileStore"; +import { useEditorStore } from "@/hooks/useEditorStore"; export interface HeaderProps { code: string; @@ -22,26 +24,57 @@ export interface HeaderProps { export default function CodeEditor() { const [Language, setLanguage] = useState(""); - const [code, setCode] = useState(""); const editorRef = useRef(null); const [output, setOutput] = useState("Terminal output will appear here..."); const [error, setError] = useState(false); - - useEffect(() => { - //@ts-ignore - setCode(CODE_SNIPPETS[Language]); - }, [Language]); + const { activeFileId, files, updateFileLanguage } = useFileStore(); + const { contents, updateContent } = useEditorStore(); + const code = activeFileId ? contents[activeFileId] ?? "" : ""; + + // Get the active file + const activeFile = activeFileId ? files.find((f) => f.id === activeFileId) : null; function handleEditorDidMount(editor: any) { editorRef.current = editor; editor.focus(); } + // Handle language changes from Header - update the active file's language + const handleLanguageChange = (newLanguage: string) => { + setLanguage(newLanguage); + if (activeFileId) { + updateFileLanguage(activeFileId, newLanguage); + } + }; + + // Sync language state with active file's language when file changes + useEffect(() => { + if (activeFile?.language) { + setLanguage(activeFile.language); + } else if (activeFile && !activeFile.language) { + // If file has no language, set default to empty (user can select) + setLanguage(""); + } + }, [activeFile?.id]); + + // Initialize code snippet when file is first opened + useEffect(() => { + if (!activeFileId || !activeFile) return; + + // Only initialize if file has no content and has a language + if (contents[activeFileId] === undefined && activeFile.language) { + const snippet = CODE_SNIPPETS[activeFile.language as keyof typeof CODE_SNIPPETS] ?? ""; + if (snippet) { + updateContent(activeFileId, snippet); + } + } + }, [activeFileId, activeFile?.language]); + return (
- + {/* Main Editor Panel updated with gradient, shadow, and side borders */} @@ -65,7 +98,7 @@ export default function CodeEditor() { >
@@ -77,7 +110,9 @@ export default function CodeEditor() { defaultValue="Start typing your code here..." onMount={handleEditorDidMount} value={code} - onChange={(value) => setCode(value || "")} + onChange={(val) => + activeFileId && updateContent(activeFileId, val || "") + } options={{ minimap: { enabled: false }, padding: { bottom: 4, top: 6 }, diff --git a/remote-ide/package-lock.json b/remote-ide/package-lock.json index def7dc8..d9249c8 100644 --- a/remote-ide/package-lock.json +++ b/remote-ide/package-lock.json @@ -39,8 +39,10 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "use-sound": "^5.0.0", + "uuid": "^13.0.0", "vscode-icons-js": "^11.6.1", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.9" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -9517,6 +9519,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vest-utils": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/vest-utils/-/vest-utils-1.3.3.tgz", @@ -9817,6 +9832,35 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/remote-ide/package.json b/remote-ide/package.json index 96d4816..3afea43 100644 --- a/remote-ide/package.json +++ b/remote-ide/package.json @@ -40,8 +40,10 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "use-sound": "^5.0.0", + "uuid": "^13.0.0", "vscode-icons-js": "^11.6.1", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.9" }, "devDependencies": { "@eslint/eslintrc": "^3", From 8dee411d8d9c7d9735dfc082e96f9a222d9a868f Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:46:23 +0530 Subject: [PATCH 02/11] Enhance FileExplorer components with improved Zustand integration for better state management. Refined file and folder interactions in FileCom and FolderCom, and ensured CodeEditor accurately reflects active file content and language. Updated dependencies accordingly. --- remote-ide/hooks/useEditorStore.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 remote-ide/hooks/useEditorStore.tsx diff --git a/remote-ide/hooks/useEditorStore.tsx b/remote-ide/hooks/useEditorStore.tsx new file mode 100644 index 0000000..f868ebc --- /dev/null +++ b/remote-ide/hooks/useEditorStore.tsx @@ -0,0 +1,17 @@ +import { create } from "zustand"; + +type EditorStore = { + contents: Record; + updateContent: (fileId: string, code: string) => void; +}; + +export const useEditorStore = create((set) => ({ + contents: {}, + updateContent: (fileId, code) => + set((state) => ({ + contents: { + ...state.contents, + [fileId]: code, + }, + })), +})); From b24ac543d5426bc0a642cc9d349f65ffb5263571 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:47:48 +0530 Subject: [PATCH 03/11] Refactor FileExplorer components to enhance Zustand state management, improving file and folder interactions. Updated CodeEditor for better synchronization with active file content and language. Adjusted dependencies for optimal performance. --- remote-ide/hooks/useFileStore.ts | 104 +++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 remote-ide/hooks/useFileStore.ts diff --git a/remote-ide/hooks/useFileStore.ts b/remote-ide/hooks/useFileStore.ts new file mode 100644 index 0000000..3b4f7fe --- /dev/null +++ b/remote-ide/hooks/useFileStore.ts @@ -0,0 +1,104 @@ +import { create } from "zustand"; +import { v4 as uuid } from "uuid"; + +export type FileNode = { + id: string; + name: string; + type: "file" | "folder"; + parentId: string | null; + language?: string; +}; + +// Helper function to infer language from file extension +function inferLanguageFromFileName(fileName: string): string | undefined { + const ext = fileName.split('.').pop()?.toLowerCase(); + if (!ext) return undefined; + + const extensionMap: Record = { + 'cpp': 'cpp', + 'cc': 'cpp', + 'cxx': 'cpp', + 'c': 'c', + 'js': 'javascript', + 'jsx': 'javascript', + 'py': 'python', + 'java': 'java', + 'cs': 'csharp', + 'ts': 'typescript', + 'tsx': 'typescript', + 'php': 'php', + 'go': 'go', + 'rb': 'ruby', + 'rs': 'rust', + 'kt': 'kotlin', + 'swift': 'swift', + 'sql': 'sqlite3', + 'ps1': 'powershell', + 'sh': 'bash', + 'm': 'matl', + 'dart': 'dart', + }; + + return extensionMap[ext]; +} + +type FileStore = { + files: FileNode[]; + activeFileId: string | null; + + addFile: (name: string, parentId: string) => void; + addFolder: (name: string, parentId: string) => void; + setActiveFile: (id: string) => void; + updateFileLanguage: (fileId: string, language: string) => void; +}; + +export const useFileStore = create((set) => ({ + files: [ + { + id: "root", + name: "SDE", + type: "folder", + parentId: null, + }, + ], + activeFileId: null, + + addFile: (name, parentId) => + set((state) => { + const inferredLanguage = inferLanguageFromFileName(name); + return { + files: [ + ...state.files, + { + id: uuid(), + name, + type: "file", + parentId, + language: inferredLanguage || "cpp", // Default to cpp if can't infer + }, + ], + }; + }), + + addFolder: (name, parentId) => + set((state) => ({ + files: [ + ...state.files, + { + id: uuid(), + name, + type: "folder", + parentId, + }, + ], + })), + + setActiveFile: (id) => set({ activeFileId: id }), + + updateFileLanguage: (fileId, language) => + set((state) => ({ + files: state.files.map((file) => + file.id === fileId ? { ...file, language } : file + ), + })), +})); From dcf28e9a7b90f688c4002979c7bfabbb1e32a3da Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:58:06 +0530 Subject: [PATCH 04/11] Update FolderCom styles for improved UI consistency by adding padding, rounded corners, and text color to the Plus button. --- remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index b0253bc..6945e9e 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -33,7 +33,7 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { {folder.name} { e.stopPropagation(); setShowPopup(true); From 0a1f008fdb28d2171e878cde8ca939e25a5eb8fe Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 23:07:20 +0530 Subject: [PATCH 05/11] Implement AddItemPopup for file and folder creation in FileExplorer, enhancing user interaction. Refactor FolderCom to utilize the new popup for adding items, improving UI consistency and state management. --- .../[id]/FileExplorer/FileExplorer.tsx | 32 ++++++++++++++++--- .../[id]/FileExplorer/FolderCom.tsx | 28 ++++++---------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx index 48122cd..4a4ab44 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx @@ -1,20 +1,44 @@ // FileExplorer.tsx +import { useState } from "react"; import { FilePlus, FolderPlus, Search } from "lucide-react"; import { FolderCom } from "./FolderCom"; +import { AddItemPopup } from "./AddItemPopup"; export const FileExplorer = () => { + const [showPopup, setShowPopup] = useState(false); + const [popupType, setPopupType] = useState<"file" | "folder">("file"); + return (
-
+
- Files + Files
- - + { + setPopupType("folder"); + setShowPopup(true); + }} + /> + { + setPopupType("file"); + setShowPopup(true); + }} + />
+ {showPopup && ( + setShowPopup(false)} + /> + )}
diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index 6945e9e..2e96eaa 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -2,29 +2,21 @@ import { useState } from "react"; import { Folder, ChevronDown, ChevronRight, Plus } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; import { FileCom } from "./FileCom"; +import { AddItemPopup } from "./AddItemPopup"; export const FolderCom = ({ folderId }: { folderId: string }) => { const [open, setOpen] = useState(true); const [showPopup, setShowPopup] = useState(false); - const [name, setName] = useState(""); - const [type, setType] = useState<"file" | "folder">("file"); - const { files, addFile, addFolder } = useFileStore(); + const { files } = useFileStore(); const children = files.filter((f) => f.parentId === folderId); const folder = files.find((f) => f.id === folderId); if (!folder) return null; - const handleAdd = () => { - if (!name.trim()) return; - type === "file" ? addFile(name, folderId) : addFolder(name, folderId); - setName(""); - setShowPopup(false); - }; - return ( -
+
setOpen(!open)} @@ -33,7 +25,7 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { {folder.name} { e.stopPropagation(); setShowPopup(true); @@ -54,13 +46,11 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { )} {showPopup && ( -
- setName(e.target.value)} /> - - +
+ setShowPopup(false)} + />
)}
From f2058eec4e84e6ade1c3fdbe973d2f5bfbbeb505 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 23:07:41 +0530 Subject: [PATCH 06/11] add file structure --- .../[id]/FileExplorer/AddItemPopup.tsx | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx new file mode 100644 index 0000000..edeb01a --- /dev/null +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect, useRef } from "react"; +import { X } from "lucide-react"; +import { useFileStore } from "../../../../hooks/useFileStore"; + +// Invalid characters for file/folder names (Windows + Unix) +const INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/; +const INVALID_NAMES = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; + +interface AddItemPopupProps { + parentId: string; + initialType?: "file" | "folder"; + onClose: () => void; +} + +export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddItemPopupProps) => { + const [name, setName] = useState(""); + const [type, setType] = useState<"file" | "folder">(initialType); + const [error, setError] = useState(""); + const inputRef = useRef(null); + const popupRef = useRef(null); + + const { files, addFile, addFolder } = useFileStore(); + + // Focus input when popup opens + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + // Close popup when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [onClose]); + + const children = files.filter((f) => f.parentId === parentId); + + const validateName = (fileName: string, isFile: boolean): string | null => { + const trimmed = fileName.trim(); + + if (!trimmed) { + return "Name cannot be empty"; + } + + if (trimmed.length > 255) { + return "Name is too long (max 255 characters)"; + } + + if (INVALID_CHARS.test(trimmed)) { + return "Name contains invalid characters: < > : \" / \\ | ? * and control characters"; + } + + // Check for invalid Windows names + const nameUpper = trimmed.toUpperCase(); + if (INVALID_NAMES.includes(nameUpper) || INVALID_NAMES.some(invalid => nameUpper.startsWith(invalid + '.'))) { + return "This name is reserved and cannot be used"; + } + + // Check for leading/trailing spaces or dots (Windows) + if (trimmed.startsWith('.') || trimmed.endsWith('.') || trimmed.endsWith(' ')) { + return "Name cannot start with a dot or end with a dot or space"; + } + + // Check for duplicate names in the same folder + const siblingNames = children.map(c => c.name.toLowerCase()); + if (siblingNames.includes(trimmed.toLowerCase())) { + return "A file or folder with this name already exists"; + } + + return null; + }; + + const handleAdd = () => { + const validationError = validateName(name, type === "file"); + + if (validationError) { + setError(validationError); + return; + } + + const trimmedName = name.trim(); + if (type === "file") { + addFile(trimmedName, parentId); + } else { + addFolder(trimmedName, parentId); + } + + onClose(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAdd(); + } else if (e.key === "Escape") { + onClose(); + } + }; + + return ( +
+
+ + New {type === "file" ? "File" : "Folder"} + + +
+ + { + setName(e.target.value); + setError(""); + }} + onKeyDown={handleKeyDown} + placeholder={`Enter ${type === "file" ? "file" : "folder"} name`} + className="w-full px-2 py-1 bg-gray-900 border border-gray-600 rounded text-white text-sm mb-2 focus:outline-none focus:border-blue-500" + /> + + + + {error && ( +
{error}
+ )} + +
+ + +
+
+ ); +}; + From efc9d1bf9bd258d826aaf6ea9956d1919c8a3bee Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 13 Dec 2025 23:17:11 +0530 Subject: [PATCH 07/11] rename Popupadded --- .../[id]/FileExplorer/RenamePopup.tsx | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx new file mode 100644 index 0000000..e406259 --- /dev/null +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx @@ -0,0 +1,168 @@ +import { useState, useEffect, useRef } from "react"; +import { X } from "lucide-react"; +import { useFileStore } from "../../../../hooks/useFileStore"; + +// Invalid characters for file/folder names (Windows + Unix) +const INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/; +const INVALID_NAMES = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; + +interface RenamePopupProps { + itemId: string; + currentName: string; + onClose: () => void; +} + +export const RenamePopup = ({ itemId, currentName, onClose }: RenamePopupProps) => { + const [name, setName] = useState(currentName); + const [error, setError] = useState(""); + const inputRef = useRef(null); + const popupRef = useRef(null); + + const { files, renameFile } = useFileStore(); + + // Focus input when popup opens and select the name part (without extension for files) + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + // Select name without extension for easier renaming + const lastDotIndex = currentName.lastIndexOf('.'); + if (lastDotIndex > 0) { + inputRef.current.setSelectionRange(0, lastDotIndex); + } else { + inputRef.current.select(); + } + } + }, [currentName]); + + // Close popup when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [onClose]); + + const item = files.find(f => f.id === itemId); + if (!item) return null; + + const parentId = item.parentId; + const siblings = files.filter(f => f.parentId === parentId && f.id !== itemId); + + const validateName = (fileName: string): string | null => { + const trimmed = fileName.trim(); + + if (!trimmed) { + return "Name cannot be empty"; + } + + if (trimmed.length > 255) { + return "Name is too long (max 255 characters)"; + } + + if (INVALID_CHARS.test(trimmed)) { + return "Name contains invalid characters: < > : \" / \\ | ? * and control characters"; + } + + // Check for invalid Windows names + const nameUpper = trimmed.toUpperCase(); + if (INVALID_NAMES.includes(nameUpper) || INVALID_NAMES.some(invalid => nameUpper.startsWith(invalid + '.'))) { + return "This name is reserved and cannot be used"; + } + + // Check for leading/trailing spaces or dots (Windows) + if (trimmed.startsWith('.') || trimmed.endsWith('.') || trimmed.endsWith(' ')) { + return "Name cannot start with a dot or end with a dot or space"; + } + + // Check for duplicate names in the same folder + const siblingNames = siblings.map(c => c.name.toLowerCase()); + if (siblingNames.includes(trimmed.toLowerCase())) { + return "A file or folder with this name already exists"; + } + + return null; + }; + + const handleRename = () => { + if (name.trim() === currentName) { + onClose(); + return; + } + + const validationError = validateName(name); + + if (validationError) { + setError(validationError); + return; + } + + renameFile(itemId, name.trim()); + onClose(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleRename(); + } else if (e.key === "Escape") { + onClose(); + } + }; + + return ( +
+
+ + Rename {item.type === "file" ? "File" : "Folder"} + + +
+ + { + setName(e.target.value); + setError(""); + }} + onKeyDown={handleKeyDown} + placeholder="Enter new name" + className="w-full px-2 py-1 bg-gray-900 border border-gray-600 rounded text-white text-sm mb-2 focus:outline-none focus:border-blue-500" + /> + + {error && ( +
{error}
+ )} + +
+ + +
+
+ ); +}; + From 226c6fac1b992748730c11033f9d579cd4b31858 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:17:55 +0530 Subject: [PATCH 08/11] buttton fixes --- .../CodeEditor/[id]/FileExplorer/FileCom.tsx | 37 +++++++++++++---- .../[id]/FileExplorer/FolderCom.tsx | 41 +++++++++++++++---- .../[id]/FileExplorer/RenamePopup.tsx | 3 +- remote-ide/hooks/useFileStore.ts | 22 ++++++++++ 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx index a4ee279..f40194a 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx @@ -1,19 +1,42 @@ -import { File } from "lucide-react"; +import { useState } from "react"; +import { File, Pencil } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; +import { RenamePopup } from "./RenamePopup"; export const FileCom = ({ fileId }: { fileId: string }) => { + const [showRenamePopup, setShowRenamePopup] = useState(false); const { files, setActiveFile } = useFileStore(); const file = files.find((f) => f.id === fileId); if (!file) return null; return ( -
setActiveFile(fileId)} - > - - {file.name} +
+
setActiveFile(fileId)} + > + + {file.name} + { + e.stopPropagation(); + setShowRenamePopup(true); + }} + /> +
+ + {showRenamePopup && ( +
+ setShowRenamePopup(false)} + /> +
+ )}
); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index 2e96eaa..043eb3a 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -1,12 +1,14 @@ import { useState } from "react"; -import { Folder, ChevronDown, ChevronRight, Plus } from "lucide-react"; +import { Folder, ChevronDown, ChevronRight, Plus, Pencil } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; import { FileCom } from "./FileCom"; import { AddItemPopup } from "./AddItemPopup"; +import { RenamePopup } from "./RenamePopup"; export const FolderCom = ({ folderId }: { folderId: string }) => { const [open, setOpen] = useState(true); const [showPopup, setShowPopup] = useState(false); + const [showRenamePopup, setShowRenamePopup] = useState(false); const { files } = useFileStore(); @@ -18,19 +20,30 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { return (
setOpen(!open)} > {open ? : } {folder.name} - { - e.stopPropagation(); - setShowPopup(true); - }} - /> +
+ { + e.stopPropagation(); + setShowRenamePopup(true); + }} + /> + { + e.stopPropagation(); + setShowPopup(true); + }} + /> +
{open && ( @@ -53,6 +66,16 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { />
)} + + {showRenamePopup && ( +
+ setShowRenamePopup(false)} + /> +
+ )}
); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx index e406259..1b96161 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx @@ -116,8 +116,7 @@ export const RenamePopup = ({ itemId, currentName, onClose }: RenamePopupProps) return (
diff --git a/remote-ide/hooks/useFileStore.ts b/remote-ide/hooks/useFileStore.ts index 3b4f7fe..3604e5f 100644 --- a/remote-ide/hooks/useFileStore.ts +++ b/remote-ide/hooks/useFileStore.ts @@ -50,6 +50,7 @@ type FileStore = { addFolder: (name: string, parentId: string) => void; setActiveFile: (id: string) => void; updateFileLanguage: (fileId: string, language: string) => void; + renameFile: (fileId: string, newName: string) => void; }; export const useFileStore = create((set) => ({ @@ -101,4 +102,25 @@ export const useFileStore = create((set) => ({ file.id === fileId ? { ...file, language } : file ), })), + + renameFile: (fileId, newName) => + set((state) => { + // Update language if it's a file and extension changed + const file = state.files.find((f) => f.id === fileId); + const inferredLanguage = file?.type === "file" + ? inferLanguageFromFileName(newName) + : undefined; + + return { + files: state.files.map((file) => + file.id === fileId + ? { + ...file, + name: newName, + ...(inferredLanguage && { language: inferredLanguage }), + } + : file + ), + }; + }), })); From 1384b07e6a18b4b3d3f1d07edee5242813389fe0 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:30:24 +0530 Subject: [PATCH 09/11] updates --- remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx | 5 +++++ remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx | 4 +++- remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx | 2 ++ remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx | 2 ++ remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx | 5 +++++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx index edeb01a..d7c3fa2 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState, useEffect, useRef } from "react"; import { X } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; @@ -30,6 +32,9 @@ export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddIte // Close popup when clicking outside useEffect(() => { + // Only run on client + if (typeof window === "undefined") return; + const handleClickOutside = (event: MouseEvent) => { if (popupRef.current && !popupRef.current.contains(event.target as Node)) { onClose(); diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx index f40194a..d4f2ca4 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState } from "react"; import { File, Pencil } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; @@ -19,7 +21,7 @@ export const FileCom = ({ fileId }: { fileId: string }) => { {file.name} { e.stopPropagation(); diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx index 4a4ab44..6e156c4 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx @@ -1,3 +1,5 @@ +"use client"; + // FileExplorer.tsx import { useState } from "react"; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index 043eb3a..17e5a52 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState } from "react"; import { Folder, ChevronDown, ChevronRight, Plus, Pencil } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx index 1b96161..46d698f 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState, useEffect, useRef } from "react"; import { X } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; @@ -36,6 +38,9 @@ export const RenamePopup = ({ itemId, currentName, onClose }: RenamePopupProps) // Close popup when clicking outside useEffect(() => { + // Only run on client + if (typeof window === "undefined") return; + const handleClickOutside = (event: MouseEvent) => { if (popupRef.current && !popupRef.current.contains(event.target as Node)) { onClose(); From 4b06fcb1bc76ffa63223fc27231b4a1fd120b2a1 Mon Sep 17 00:00:00 2001 From: Avan14 <145996250+Avan14@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:22:46 +0530 Subject: [PATCH 10/11] data flow re-designed --- remote-ide/app/Api/User/modals/User.ts | 37 -- remote-ide/app/Api/User/route.ts | 83 ++--- remote-ide/app/Api/User/schema/types.ts | 80 ----- .../app/Api/documents/[documentId]/route.ts | 0 remote-ide/app/Api/documents/route.ts | 0 .../app/Api/projects/[projectId]/route.ts | 74 ++++ remote-ide/app/Api/projects/route.ts | 80 +++++ remote-ide/app/CodeEditor/[id]/CloudApi.tsx | 25 +- .../[id]/FileExplorer/AddItemPopup.tsx | 85 ++--- .../[id]/FileExplorer/DeletePopup.tsx | 100 ++++++ .../CodeEditor/[id]/FileExplorer/FileCom.tsx | 24 +- .../[id]/FileExplorer/FileExplorer.tsx | 22 +- .../[id]/FileExplorer/FolderCom.tsx | 32 +- .../[id]/FileExplorer/RenamePopup.tsx | 52 +-- .../app/CodeEditor/[id]/FileExplorer/types.ts | 98 ++++-- remote-ide/app/CodeEditor/[id]/Header.tsx | 105 ++---- remote-ide/app/CodeEditor/[id]/api.tsx | 4 +- remote-ide/app/CodeEditor/[id]/chatbot.tsx | 9 +- remote-ide/app/CodeEditor/[id]/constants.ts | 47 +-- remote-ide/app/CodeEditor/[id]/layout.tsx | 13 +- remote-ide/app/CodeEditor/[id]/page.tsx | 97 ++++-- remote-ide/app/DashBoard/CreateProject.tsx | 96 ++--- remote-ide/app/DashBoard/ProjectCard.tsx | 134 +++---- remote-ide/app/DashBoard/page.tsx | 36 +- remote-ide/app/DashBoard/sidebar.tsx | 50 +-- remote-ide/app/DashBoard/types.ts | 20 +- .../[id]/FileExplorer/FileCom.tsx | 0 .../[id]/FileExplorer/FileExplorer.tsx | 0 .../[id]/FileExplorer/FolderCom.tsx | 0 .../[id]/FileExplorer/types.ts | 0 .../app/{WebDev => WebEditor}/[id]/Header.tsx | 0 .../{WebDev => WebEditor}/[id]/chatbot.tsx | 9 +- .../app/{WebDev => WebEditor}/[id]/layout.tsx | 0 .../app/{WebDev => WebEditor}/[id]/page.tsx | 4 +- remote-ide/app/context/UserDataContext.tsx | 31 -- remote-ide/app/layout.tsx | 19 +- remote-ide/app/page.tsx | 2 +- remote-ide/components/Constants/constants.ts | 2 +- remote-ide/components/elements/ChatbotRes.tsx | 26 +- remote-ide/components/elements/Footer.tsx | 2 +- remote-ide/components/elements/Hero2.tsx | 2 +- remote-ide/components/elements/Navbar.tsx | 20 +- remote-ide/hooks/useEditorStore.ts | 34 ++ remote-ide/hooks/useEditorStore.tsx | 17 - remote-ide/hooks/useFileStore.ts | 329 ++++++++++++++---- remote-ide/hooks/useUserStore.ts | 22 ++ remote-ide/middleware.ts | 2 +- remote-ide/package-lock.json | 10 + remote-ide/package.json | 1 + remote-ide/server/db/mongoose.ts | 34 ++ remote-ide/server/models/models.ts | 50 +++ 51 files changed, 1203 insertions(+), 816 deletions(-) delete mode 100644 remote-ide/app/Api/User/modals/User.ts delete mode 100644 remote-ide/app/Api/User/schema/types.ts create mode 100644 remote-ide/app/Api/documents/[documentId]/route.ts create mode 100644 remote-ide/app/Api/documents/route.ts create mode 100644 remote-ide/app/Api/projects/[projectId]/route.ts create mode 100644 remote-ide/app/Api/projects/route.ts create mode 100644 remote-ide/app/CodeEditor/[id]/FileExplorer/DeletePopup.tsx rename remote-ide/app/{WebDev => WebEditor}/[id]/FileExplorer/FileCom.tsx (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/FileExplorer/FileExplorer.tsx (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/FileExplorer/FolderCom.tsx (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/FileExplorer/types.ts (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/Header.tsx (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/chatbot.tsx (93%) rename remote-ide/app/{WebDev => WebEditor}/[id]/layout.tsx (100%) rename remote-ide/app/{WebDev => WebEditor}/[id]/page.tsx (96%) delete mode 100644 remote-ide/app/context/UserDataContext.tsx create mode 100644 remote-ide/hooks/useEditorStore.ts delete mode 100644 remote-ide/hooks/useEditorStore.tsx create mode 100644 remote-ide/hooks/useUserStore.ts create mode 100644 remote-ide/server/db/mongoose.ts create mode 100644 remote-ide/server/models/models.ts diff --git a/remote-ide/app/Api/User/modals/User.ts b/remote-ide/app/Api/User/modals/User.ts deleted file mode 100644 index d0e6160..0000000 --- a/remote-ide/app/Api/User/modals/User.ts +++ /dev/null @@ -1,37 +0,0 @@ -import mongoose from "mongoose"; - -const FileFolderSchema = new mongoose.Schema({ - id: String, - name: String, - type: { - type: String, - enum: ["File", "Folder"], - required: true, - }, - code: String, - children: [mongoose.Schema.Types.Mixed], // placeholder to allow nesting -}); - -// This sets children as an array of the same schema (recursive) -FileFolderSchema.add({ - children: [FileFolderSchema], -}); - -const ProjectSchema = new mongoose.Schema({ - project_id: String, - name: String, - type: String, - date: Date, - language: String, - folder: FileFolderSchema, -}); - -const UserSchema = new mongoose.Schema({ - id: String, - name: String, - projects: [ProjectSchema], -}); - -const UserModel = mongoose.models.User || mongoose.model("User", UserSchema); - -export default UserModel; diff --git a/remote-ide/app/Api/User/route.ts b/remote-ide/app/Api/User/route.ts index c759dff..d4750a0 100644 --- a/remote-ide/app/Api/User/route.ts +++ b/remote-ide/app/Api/User/route.ts @@ -1,71 +1,40 @@ -import mongoose from "mongoose"; -import UserModel from "./modals/User"; -import { NextRequest, NextResponse } from "next/server"; - -// Connect to MongoDB -async function connectDB() { - try { - await mongoose.connect(process.env.MONGODB_URI as string); - console.log("Connected to MongoDB"); - } catch (error) { - console.error("MongoDB connection error:", error); - throw error; - } -} -await connectDB(); +import { NextRequest, NextResponse } from "next/server" +import { auth } from "@clerk/nextjs/server" +import { connectToDB } from "@/server/db/mongoose" +import { User } from "@/server/models/models" export async function POST(req: NextRequest) { - const body = await req.json(); - const { userId, name } = body; + const { userId } = await auth() - try { - - // Check if user exists - const existingUser = await UserModel.findOne({ id: userId }); - if (existingUser) { - return NextResponse.json(existingUser, { status: 200 }); - } - - // Create new user if not exists - const newUser = { - id: userId, - name: name, - projects: [], - }; - - const createdUser = await UserModel.create(newUser); - - return NextResponse.json(createdUser, { status: 200 }); - } catch (error) { - console.error("Error creating user:", error); - return NextResponse.json({ error: "Server error" }, { status: 500 }); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) } -} + const body = await req.json().catch(() => ({})) + const name = body?.name ?? "User" -// end-point for updating projects in the userd database -export async function PUT(req: NextRequest) { try { - const body = await req.json(); - const { userId, updatedprojects } = body; - - if (!userId || !Array.isArray(updatedprojects)) { - return NextResponse.json({ error: "Invalid data" }, { status: 400 }); - } + await connectToDB(); - const updatedUser = await UserModel.findOneAndUpdate( - { id: userId }, - { $set: { projects: updatedprojects } }, - { new: true } - ); + let user = await User.findOne({ authId: userId }) - if (!updatedUser) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); + if (!user) { + user = await User.create({ + authId: userId, + name, + }) } - return NextResponse.json({ message: "User updated", user: updatedUser }, { status: 200 }); + return NextResponse.json( + { + id: user._id.toString(), + name: user.name, + createdAt: user.createdAt, + }, + { status: 201 } + ) } catch (err) { - console.error("Error updating user:", err); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + console.error("User init error:", err) + return NextResponse.json({ error: "Server error" }, { status: 500 }) } } diff --git a/remote-ide/app/Api/User/schema/types.ts b/remote-ide/app/Api/User/schema/types.ts deleted file mode 100644 index efd7875..0000000 --- a/remote-ide/app/Api/User/schema/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { z } from "zod"; - -export type FileTreeType = { - id: string; - name: string; - type: "Folder" | "File"; - code: string; - children: FileTreeType[]; -}; - -const FileTreeSchemaType: z.ZodType = z.lazy(() => z.object({ - id: z.string(), - name: z.string(), - type: z.enum(["Folder", "File"]), - code: z.string(), - children: z.array(FileTreeSchemaType), -})); - -const ProjectSchemaType = z.object({ - project_id: z.string(), - name: z.string(), - type: z.string(), - date: z.coerce.date(), - language: z.string(), - folder: FileTreeSchemaType, -}); - -const UserSchemaType = z.object({ - id: z.string(), - name: z.string(), - projects: z.array(ProjectSchemaType), -}); - -export { FileTreeSchemaType, ProjectSchemaType, UserSchemaType }; - -const exampleUser = { - id: "user_123", - name: "John Doe", - projects: [ - { - project_id: "proj_456", - name: "AI Chatbot", - type: "Web Application", - date: new Date("2024-04-03"), - language: "TypeScript", - folder: { - id: "folder_1", - name: "src", - type: "Folder", - code: "", - children: [ - { - id: "file_1", - name: "index.ts", - type: "File", - code: "console.log('Hello, World!');", - children: [] - }, - { - id: "folder_2", - name: "components", - type: "Folder", - code: "", - children: [ - { - id: "file_2", - name: "Chatbot.tsx", - type: "File", - code: "export function Chatbot() { return
Chatbot
; }", - children: [] - } - ] - } - ] - } - } - ] -}; - - diff --git a/remote-ide/app/Api/documents/[documentId]/route.ts b/remote-ide/app/Api/documents/[documentId]/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/remote-ide/app/Api/documents/route.ts b/remote-ide/app/Api/documents/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/remote-ide/app/Api/projects/[projectId]/route.ts b/remote-ide/app/Api/projects/[projectId]/route.ts new file mode 100644 index 0000000..66fc32a --- /dev/null +++ b/remote-ide/app/Api/projects/[projectId]/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from "next/server" +import { auth } from "@clerk/nextjs/server" +import { connectToDB } from "@/server/db/mongoose" +import { Project, Document } from "@/server/models/models" +import mongoose from "mongoose" + +// builds nested list +function buildTree(nodes: any[]) { + const map = new Map() + let root = null + + for (const node of nodes) { + map.set(node._id.toString(), { + id: node._id.toString(), + name: node.name, + type: node.type, + extension: node.extension, + children: [], + }) + } + + for (const node of nodes) { + const id = node._id.toString() + if (node.parent) { + const parent = map.get(node.parent.toString()) + parent.children.push(map.get(id)) + } else { + root = map.get(id) + } + } + return root; +} + +export async function GET( + req: NextRequest, + ctx: { params: Promise<{ projectId: string }> } +) { + const { userId } = await auth() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + + const { projectId } = await ctx.params + + if (!mongoose.Types.ObjectId.isValid(projectId)) { + return NextResponse.json({ error: "Invalid project id" }, { status: 400 }) + } + + await connectToDB() + + const project = await Project.findById(projectId).populate("owner") + + if (!project || project.owner.authId !== userId) { + return NextResponse.json({ error: "Not found" }, { status: 404 }) + } + + const documents = await Document.find({ + project: project._id, + isDeleted: false, + }).lean() + + const tree = buildTree(documents) + + console.log(tree); + return NextResponse.json({ + project: { + id: project._id.toString(), + name: project.name, + track: project.track, + }, + tree, + }) +} + diff --git a/remote-ide/app/Api/projects/route.ts b/remote-ide/app/Api/projects/route.ts new file mode 100644 index 0000000..22b58cc --- /dev/null +++ b/remote-ide/app/Api/projects/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@clerk/nextjs/server' +import slugify from 'slugify' +import { connectToDB } from '@/server/db/mongoose' +import { Project, User } from '@/server/models/models' + +export async function GET(req: NextRequest) { + try { + await connectToDB(); + + const { userId } = await auth() + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const user = await User.findOne({ authId: userId }) + if (!user) { + return NextResponse.json([], { status: 200 }) + } + + const projects = await Project.find({ owner: user._id }) + .sort({ createdAt: -1 }) + .select('_id name slug track createdAt') + + return NextResponse.json( + projects.map(p => ({ + id: p._id.toString(), + name: p.name, + slug: p.slug, + track: p.track, + createdAt: p.createdAt, + })) + ) + } + catch { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} +export async function PUT(req: NextRequest) { + try { + + await connectToDB() + const { userId } = await auth() + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { name, track } = await req.json() + + const user = await User.findOne({ authId: userId }) + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + const project = await Project.create({ + owner: user._id, + name, + track, + slug: slugify(name, { lower: true, strict: true }), + }) + + return NextResponse.json({ + id: project._id.toString(), + name: project.name, + slug: project.slug, + track: project.track, + createdAt: project.createdAt, + }) + } catch (err: any) { + if (err.code === 11000) { + return NextResponse.json( + { error: 'Project with same name already exists' }, + { status: 409 } + ) + } + + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + diff --git a/remote-ide/app/CodeEditor/[id]/CloudApi.tsx b/remote-ide/app/CodeEditor/[id]/CloudApi.tsx index a038011..8f3408e 100644 --- a/remote-ide/app/CodeEditor/[id]/CloudApi.tsx +++ b/remote-ide/app/CodeEditor/[id]/CloudApi.tsx @@ -3,23 +3,26 @@ import { LANGUAGE_VERSIONS } from "./constants"; // const BASE_URL = process.env.CLOUD_URL as string; + +// this should hit the backend first then goto the cloud type Language = keyof typeof LANGUAGE_VERSIONS; - const ExecuteCode = async ( - language: Language, +const ExecuteCode = async ( + extension: string, SourceCode: string, ) => { - console.log("hi"); - // console.log(BASE_URL); + console.log("hi"); + // console.log(BASE_URL); try { console.log(SourceCode); - const response = await axios.post( "http://143.110.245.12:3000/run", { - code: SourceCode, - }, { - headers: { - "Content-Type": "application/json" - } - }); + const response = await axios.post("http://143.110.245.12:3000/run", { + code: SourceCode, + extension, + }, { + headers: { + "Content-Type": "application/json" + } + }); return response.data.output || response.data.error || "No output."; } catch (error: any) { diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx index d7c3fa2..c93657a 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/AddItemPopup.tsx @@ -1,27 +1,29 @@ +//AddItemPopup.tsx + "use client"; import { useState, useEffect, useRef } from "react"; import { X } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; -// Invalid characters for file/folder names (Windows + Unix) -const INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/; -const INVALID_NAMES = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; - interface AddItemPopupProps { parentId: string; initialType?: "file" | "folder"; onClose: () => void; } -export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddItemPopupProps) => { +export const AddItemPopup = ({ + parentId, + initialType = "file", + onClose, +}: AddItemPopupProps) => { const [name, setName] = useState(""); const [type, setType] = useState<"file" | "folder">(initialType); const [error, setError] = useState(""); const inputRef = useRef(null); const popupRef = useRef(null); - const { files, addFile, addFolder } = useFileStore(); + const { addFile, addFolder } = useFileStore(); // Focus input when popup opens useEffect(() => { @@ -36,7 +38,10 @@ export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddIte if (typeof window === "undefined") return; const handleClickOutside = (event: MouseEvent) => { - if (popupRef.current && !popupRef.current.contains(event.target as Node)) { + if ( + popupRef.current && + !popupRef.current.contains(event.target as Node) + ) { onClose(); } }; @@ -45,58 +50,13 @@ export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddIte return () => document.removeEventListener("mousedown", handleClickOutside); }, [onClose]); - const children = files.filter((f) => f.parentId === parentId); - - const validateName = (fileName: string, isFile: boolean): string | null => { - const trimmed = fileName.trim(); - - if (!trimmed) { - return "Name cannot be empty"; - } - - if (trimmed.length > 255) { - return "Name is too long (max 255 characters)"; - } - - if (INVALID_CHARS.test(trimmed)) { - return "Name contains invalid characters: < > : \" / \\ | ? * and control characters"; - } - - // Check for invalid Windows names - const nameUpper = trimmed.toUpperCase(); - if (INVALID_NAMES.includes(nameUpper) || INVALID_NAMES.some(invalid => nameUpper.startsWith(invalid + '.'))) { - return "This name is reserved and cannot be used"; - } - - // Check for leading/trailing spaces or dots (Windows) - if (trimmed.startsWith('.') || trimmed.endsWith('.') || trimmed.endsWith(' ')) { - return "Name cannot start with a dot or end with a dot or space"; - } - - // Check for duplicate names in the same folder - const siblingNames = children.map(c => c.name.toLowerCase()); - if (siblingNames.includes(trimmed.toLowerCase())) { - return "A file or folder with this name already exists"; - } - - return null; - }; - const handleAdd = () => { - const validationError = validateName(name, type === "file"); - - if (validationError) { - setError(validationError); + const result = + type === "file" ? addFile(name, parentId) : addFolder(name, parentId); + if (!result.ok) { + setError(result.error); return; } - - const trimmedName = name.trim(); - if (type === "file") { - addFile(trimmedName, parentId); - } else { - addFolder(trimmedName, parentId); - } - onClose(); }; @@ -110,10 +70,10 @@ export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddIte }; return ( -
@@ -126,7 +86,7 @@ export const AddItemPopup = ({ parentId, initialType = "file", onClose }: AddIte
- + - + - {error && ( -
{error}
- )} + {error &&
{error}
}
+
+ +
{description}
+ + {error &&
{error}
} + +
+ + +
+
+ ); +}; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx index d4f2ca4..0cc8f73 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileCom.tsx @@ -1,12 +1,15 @@ +//FileCom.tsx "use client"; import { useState } from "react"; -import { File, Pencil } from "lucide-react"; +import { File, Pencil, Trash2 } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; import { RenamePopup } from "./RenamePopup"; +import { DeletePopup } from "./DeletePopup"; export const FileCom = ({ fileId }: { fileId: string }) => { const [showRenamePopup, setShowRenamePopup] = useState(false); + const [showDeletePopup, setShowDeletePopup] = useState(false); const { files, setActiveFile } = useFileStore(); const file = files.find((f) => f.id === fileId); @@ -28,6 +31,14 @@ export const FileCom = ({ fileId }: { fileId: string }) => { setShowRenamePopup(true); }} /> + { + e.stopPropagation(); + setShowDeletePopup(true); + }} + />
{showRenamePopup && ( @@ -39,6 +50,17 @@ export const FileCom = ({ fileId }: { fileId: string }) => { />
)} + + {showDeletePopup && ( +
+ setShowDeletePopup(false)} + /> +
+ )}
); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx index 6e156c4..bc56635 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FileExplorer.tsx @@ -6,10 +6,18 @@ import { useState } from "react"; import { FilePlus, FolderPlus, Search } from "lucide-react"; import { FolderCom } from "./FolderCom"; import { AddItemPopup } from "./AddItemPopup"; +import { useFileStore } from "@/hooks/useFileStore"; export const FileExplorer = () => { const [showPopup, setShowPopup] = useState(false); const [popupType, setPopupType] = useState<"file" | "folder">("file"); + const files = useFileStore((s) => s.files); + let root = files.find((f) => f.parentId === null); + if (!root) { + // additional check + root = files.find((f) => f.parentId === null); + if (!root) return null; + } return (
@@ -18,15 +26,15 @@ export const FileExplorer = () => { Files
- { setPopupType("folder"); setShowPopup(true); }} /> - { setPopupType("file"); setShowPopup(true); @@ -35,14 +43,14 @@ export const FileExplorer = () => {
{showPopup && ( - setShowPopup(false)} /> )}
- +
); -}; \ No newline at end of file +}; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx index 17e5a52..e7dd0eb 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/FolderCom.tsx @@ -1,16 +1,19 @@ +// FolderCom.tsx "use client"; import { useState } from "react"; -import { Folder, ChevronDown, ChevronRight, Plus, Pencil } from "lucide-react"; +import { Folder, ChevronDown, ChevronRight, Plus, Pencil, Trash2 } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; import { FileCom } from "./FileCom"; import { AddItemPopup } from "./AddItemPopup"; import { RenamePopup } from "./RenamePopup"; +import { DeletePopup } from "./DeletePopup"; export const FolderCom = ({ folderId }: { folderId: string }) => { const [open, setOpen] = useState(true); - const [showPopup, setShowPopup] = useState(false); + const [showAddPopup, setShowAddPopup] = useState(false); const [showRenamePopup, setShowRenamePopup] = useState(false); + const [showDeletePopup, setShowDeletePopup] = useState(false); const { files } = useFileStore(); @@ -37,12 +40,20 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { setShowRenamePopup(true); }} /> + { + e.stopPropagation(); + setShowDeletePopup(true); + }} + /> { e.stopPropagation(); - setShowPopup(true); + setShowAddPopup(true); }} />
@@ -60,11 +71,11 @@ export const FolderCom = ({ folderId }: { folderId: string }) => {
)} - {showPopup && ( + {showAddPopup && (
setShowPopup(false)} + onClose={() => setShowAddPopup(false)} />
)} @@ -78,6 +89,17 @@ export const FolderCom = ({ folderId }: { folderId: string }) => { />
)} + + {showDeletePopup && ( +
+ setShowDeletePopup(false)} + /> +
+ )}
); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx index 46d698f..3d5e66a 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/RenamePopup.tsx @@ -1,13 +1,11 @@ +// RenamePopup.tsx + "use client"; import { useState, useEffect, useRef } from "react"; import { X } from "lucide-react"; import { useFileStore } from "../../../../hooks/useFileStore"; -// Invalid characters for file/folder names (Windows + Unix) -const INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/; -const INVALID_NAMES = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; - interface RenamePopupProps { itemId: string; currentName: string; @@ -54,58 +52,18 @@ export const RenamePopup = ({ itemId, currentName, onClose }: RenamePopupProps) const item = files.find(f => f.id === itemId); if (!item) return null; - const parentId = item.parentId; - const siblings = files.filter(f => f.parentId === parentId && f.id !== itemId); - - const validateName = (fileName: string): string | null => { - const trimmed = fileName.trim(); - - if (!trimmed) { - return "Name cannot be empty"; - } - - if (trimmed.length > 255) { - return "Name is too long (max 255 characters)"; - } - - if (INVALID_CHARS.test(trimmed)) { - return "Name contains invalid characters: < > : \" / \\ | ? * and control characters"; - } - - // Check for invalid Windows names - const nameUpper = trimmed.toUpperCase(); - if (INVALID_NAMES.includes(nameUpper) || INVALID_NAMES.some(invalid => nameUpper.startsWith(invalid + '.'))) { - return "This name is reserved and cannot be used"; - } - - // Check for leading/trailing spaces or dots (Windows) - if (trimmed.startsWith('.') || trimmed.endsWith('.') || trimmed.endsWith(' ')) { - return "Name cannot start with a dot or end with a dot or space"; - } - - // Check for duplicate names in the same folder - const siblingNames = siblings.map(c => c.name.toLowerCase()); - if (siblingNames.includes(trimmed.toLowerCase())) { - return "A file or folder with this name already exists"; - } - - return null; - }; - const handleRename = () => { if (name.trim() === currentName) { onClose(); return; } - const validationError = validateName(name); - - if (validationError) { - setError(validationError); + const result = renameFile(itemId, name); + if (!result.ok) { + setError(result.error); return; } - renameFile(itemId, name.trim()); onClose(); }; diff --git a/remote-ide/app/CodeEditor/[id]/FileExplorer/types.ts b/remote-ide/app/CodeEditor/[id]/FileExplorer/types.ts index fc44e0f..3e45112 100644 --- a/remote-ide/app/CodeEditor/[id]/FileExplorer/types.ts +++ b/remote-ide/app/CodeEditor/[id]/FileExplorer/types.ts @@ -1,39 +1,71 @@ // types.ts -export type FileTree = { - id: string; - name: string; - type: "Folder" | "File"; - code?: string; - children?: FileTree[]; - }; - - export type ProjectType = { - project_id: string; - name: string; - type: string; - date: Date; - language: string; - folder: FileTree; - }; - - export type UserType = { - id: string; - name: string; - projects: ProjectType[]; - }; - +// user +export type UserType = { + id: string; + name: string; +}; +// projects +export type ProjectState = { + id: string + name: string + slug?: string + track: 'WebDevelopment' | 'SoftWareDevelopment' + isPrivate: boolean + createdAt: string +} +// files and folders +export type DocumentNode = { + id: string + projectId: string + parentId: string | null + type: 'file' | 'folder' + name: string + extension: string | null + isDeleted: boolean +} + + +// folder tree made from doc node +export type TreeNode = { + id: string + type: DocumentType + name: string + extension: string | null + children?: TreeNode[] +} +// file content +export type FileContentState = { + fileId: string + content: string + language: string | null + isDirty: boolean + lastSavedAt: string | null +} + +export type EditorState = { + activeFileId: string | null + openFileIds: string[] +} + +export type ProjectWorkspaceState = { + project: ProjectState + documents: Record + tree: TreeNode + files: Record + editor: EditorState +} export const Folder_Example = { - "id": "root", - "name": "SDE", - "type": "Folder", - "children": [ - { - "id": "main.cpp", - "name": "main.cpp", - "type": "File" - }, - ] + "id": "root", + "name": "SDE", + "type": "Folder", + "children": [ + { + "id": "main.cpp", + "name": "main.cpp", + "type": "File" + }, + ] } \ No newline at end of file diff --git a/remote-ide/app/CodeEditor/[id]/Header.tsx b/remote-ide/app/CodeEditor/[id]/Header.tsx index 53b28d4..0737014 100644 --- a/remote-ide/app/CodeEditor/[id]/Header.tsx +++ b/remote-ide/app/CodeEditor/[id]/Header.tsx @@ -1,10 +1,6 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +// Header.tsx +"use client" + import { Tooltip, TooltipContent, @@ -12,48 +8,51 @@ import { } from "@/components/ui/tooltip"; import { Play, Settings, Code2, BotMessageSquare } from "lucide-react"; import { useState } from "react"; -import { HeaderProps } from "./page"; import { ExecuteCode } from "./api"; -import ExecuteCodecloud from "./CloudApi"; -import { LANGUAGE_VERSIONS } from "./constants"; import Link from "next/link"; import { Chatbot } from "./chatbot"; +import { RUN_OPEN_FILE_WARNNING } from "./constants"; -export const languageOptions = Object.keys(LANGUAGE_VERSIONS).map((key) => ({ - value: key, - label: `${key.charAt(0).toUpperCase() + key.slice(1)} (${ - LANGUAGE_VERSIONS[key as keyof typeof LANGUAGE_VERSIONS] - })`, -})); +interface HeaderProps { + code: string; + language: string; + fileName?: string; + setoutput: (output: string) => void; + seterror: (error: boolean) => void; +} export const Header = ({ code, - setLanguage, - Language, + language, + fileName, setoutput, seterror, }: HeaderProps) => { const [loading, setLoading] = useState(false); - const [alert, setAlert] = useState(false); const [AIhelp, setAIhelp] = useState(false); + const isRunnable = Boolean(language); const toggleAIHelp = () => { setAIhelp(!AIhelp); }; + + const getExtensionFromFileName = (name?: string) => { + if (!name) return ""; + const idx = name.lastIndexOf("."); + if (idx < 0) return ""; + return name.slice(idx + 1).toLowerCase(); + }; + const handleRun = async () => { - if (!Language || !code) { + if (!isRunnable || !code) { seterror(true); - setAlert(true); - setTimeout(() => { - setAlert(false); - }, 3000); - setoutput("Please select a language and write some code"); + setoutput(RUN_OPEN_FILE_WARNNING); return; } setLoading(true); try { // @ts-ignore - const Sourcecode = await ExecuteCode(Language, code, seterror); + const Sourcecode = await ExecuteCode(language as any, code, seterror); setoutput(Sourcecode); seterror(false); } catch (error) { @@ -64,28 +63,8 @@ export const Header = ({ } }; const handleRuncloud = async () => { - if (!Language || !code) { - seterror(true); - setAlert(true); - setTimeout(() => { - setAlert(false); - }, 3000); - setoutput("Please select a language and write some code"); - return; - } - setLoading(true); - try { - console.log(code); - // @ts-ignore - const outputcode = await ExecuteCodecloud(Language, code, seterror); - setoutput(outputcode); - seterror(false); - } catch (error) { - setoutput("Error: " + error); - seterror(true); - } finally { - setLoading(false); - } + // LOGIC FOR HITTING THE Backend to hit cloud spin up some containers + // and return stdio / stderr }; return (
@@ -111,22 +90,6 @@ export const Header = ({
- - diff --git a/remote-ide/app/CodeEditor/[id]/api.tsx b/remote-ide/app/CodeEditor/[id]/api.tsx index 033dc5d..558bc0d 100644 --- a/remote-ide/app/CodeEditor/[id]/api.tsx +++ b/remote-ide/app/CodeEditor/[id]/api.tsx @@ -1,7 +1,7 @@ import axios from "axios"; import { LANGUAGE_VERSIONS } from "./constants"; -const ExecuteCodeAPI = axios.create({ +const ExecuteCodeapi = axios.create({ baseURL: "https://emkc.org/api/v2/piston", }); @@ -22,7 +22,7 @@ export const ExecuteCode = async ( version: LANGUAGE_VERSIONS[language], files: [{ content: SourceCode }], }) - const response = await ExecuteCodeAPI.post("/execute", { + const response = await ExecuteCodeapi.post("/execute", { language, version: LANGUAGE_VERSIONS[language], files: [{ content: SourceCode }], diff --git a/remote-ide/app/CodeEditor/[id]/chatbot.tsx b/remote-ide/app/CodeEditor/[id]/chatbot.tsx index b4a9ec4..8c9d54b 100644 --- a/remote-ide/app/CodeEditor/[id]/chatbot.tsx +++ b/remote-ide/app/CodeEditor/[id]/chatbot.tsx @@ -55,8 +55,9 @@ export function Chatbot({ code ,toggleAIHelp}: ChatbotProps) { setMessages((prev) => [...prev, newMessage]); + const thinkingId = messages.length + 2; const thinkingMessage: Message = { - id: messages.length + 2, + id: thinkingId, text: "Thinking...", sender: "ai", timestamp: new Date(), @@ -71,12 +72,12 @@ export function Chatbot({ code ,toggleAIHelp}: ChatbotProps) { const aiMessage: Message = { id: messages.length + 3, - text: AIresponse, + text: typeof AIresponse === "string" ? AIresponse : String(AIresponse), sender: "ai", timestamp: new Date(), }; - setMessages((prev) => [...prev, aiMessage]); + setMessages((prev) => [...prev.filter((m) => m.id !== thinkingId), aiMessage]); setInputMessage(""); setLoading(false); }; @@ -128,7 +129,7 @@ export function Chatbot({ code ,toggleAIHelp}: ChatbotProps) { > {/* Using ReactMarkdown to render message.*/} - {message.text} + {typeof message.text === "string" ? message.text : String(message.text)}
diff --git a/remote-ide/app/CodeEditor/[id]/constants.ts b/remote-ide/app/CodeEditor/[id]/constants.ts index fd1e07f..07c79af 100644 --- a/remote-ide/app/CodeEditor/[id]/constants.ts +++ b/remote-ide/app/CodeEditor/[id]/constants.ts @@ -1,3 +1,9 @@ +export const DEFAULT_TEXT_FOR_FILE = "Terminal output will appear here..." +export const DEFAULT_WARNING_UNKNOWN_FILES = "Warning: Unsupported or missing file extension. This file will be treated as plain text and cannot be run." +export const PLAINTEXT ="plaintext" +export const OPEN_FILE_WARNNING ="Please open a file." +export const RUN_OPEN_FILE_WARNNING ="Please open a file and write some code" + export const LANGUAGE_VERSIONS = { cpp: "10.2.0", javascript: "18.15.0", @@ -12,12 +18,13 @@ export const LANGUAGE_VERSIONS = { kotlin: "1.8.20", swift: "5.3.3", sqlite3: "3.36.0", - c:"10.2.0", + c: "10.2.0", powershell: "7.1.4", bash: "5.2.0", matl: "22.7.4", dart: "2.19.6", }; + export const CODE_SNIPPETS = { cpp: `#include \n\nint main() {\n\tstd::cout << "Hello, Avan!" << std::endl;\n\treturn 0;\n}\n`, javascript: `function greet(name) {\n\tconsole.log("Hello, " + name + "!");\n}\n\ngreet("Avan");\n`, @@ -40,27 +47,27 @@ export const CODE_SNIPPETS = { }; export const FILE_NAMES = { - cpp: "program.cpp", - javascript: "app.js", - python: "script.py", - java: "MainClass.java", - csharp: "Program.cs", - typescript: "app.ts", - php: "index.php", - go: "main.go", - ruby: "program.rb", - rust: "main.rs", - kotlin: "Application.kt", - swift: "App.swift", - sqlite3: "queries.sql", - c: "main.c", - powershell: "script.ps1", - bash: "script.sh", - matl: "function.m", - dart: "main.dart", + cpp: "program.cpp", + javascript: "app.js", + python: "script.py", + java: "MainClass.java", + csharp: "Program.cs", + typescript: "app.ts", + php: "index.php", + go: "main.go", + ruby: "program.rb", + rust: "main.rs", + kotlin: "Application.kt", + swift: "App.swift", + sqlite3: "queries.sql", + c: "main.c", + powershell: "script.ps1", + bash: "script.sh", + matl: "function.m", + dart: "main.dart", }; -export const THEMES=[ +export const THEMES = [ "vs-dark", "vs-light", "hc-black" diff --git a/remote-ide/app/CodeEditor/[id]/layout.tsx b/remote-ide/app/CodeEditor/[id]/layout.tsx index e70cf23..4b3e7a5 100644 --- a/remote-ide/app/CodeEditor/[id]/layout.tsx +++ b/remote-ide/app/CodeEditor/[id]/layout.tsx @@ -1,20 +1,19 @@ -"use client"; +"use client"; import { TooltipProvider } from "@radix-ui/react-tooltip"; import { motion } from "framer-motion"; export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - - {children} - + + {children} + ); } diff --git a/remote-ide/app/CodeEditor/[id]/page.tsx b/remote-ide/app/CodeEditor/[id]/page.tsx index d81ab46..5516df1 100644 --- a/remote-ide/app/CodeEditor/[id]/page.tsx +++ b/remote-ide/app/CodeEditor/[id]/page.tsx @@ -7,55 +7,63 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { Terminal } from "lucide-react"; import Editor from "@monaco-editor/react"; import { Header } from "./Header"; -import { CODE_SNIPPETS, FILE_NAMES, THEMES } from "./constants"; +import { + CODE_SNIPPETS, + DEFAULT_TEXT_FOR_FILE, + DEFAULT_WARNING_UNKNOWN_FILES, + FILE_NAMES, + OPEN_FILE_WARNNING, + PLAINTEXT, + THEMES, +} from "./constants"; import { Button } from "@/components/ui/button"; import { FileExplorer } from "./FileExplorer/FileExplorer"; import { useFileStore } from "@/hooks/useFileStore"; import { useEditorStore } from "@/hooks/useEditorStore"; - -export interface HeaderProps { - code: string; - setLanguage: React.Dispatch>; - Language: string; - setoutput: (output: string) => void; - seterror: (error: boolean) => void; -} +import { useParams } from "next/navigation"; export default function CodeEditor() { - const [Language, setLanguage] = useState(""); const editorRef = useRef(null); - const [output, setOutput] = useState("Terminal output will appear here..."); + const [output, setOutput] = useState(DEFAULT_TEXT_FOR_FILE); const [error, setError] = useState(false); - const { activeFileId, files, updateFileLanguage } = useFileStore(); + const activeFileId = useFileStore((state) => state.activeFileId); + const files = useFileStore((state) => state.files); + const hydrateFromBackend = useFileStore((state) => state.hydrateFromBackend); + const { contents, updateContent } = useEditorStore(); + const { id: projectId } = useParams<{ id: string }>(); + const code = activeFileId ? contents[activeFileId] ?? "" : ""; - + // Get the active file - const activeFile = activeFileId ? files.find((f) => f.id === activeFileId) : null; + const activeFile = activeFileId + ? files.find((f) => f.id === activeFileId) + : null; function handleEditorDidMount(editor: any) { editorRef.current = editor; editor.focus(); } - // Handle language changes from Header - update the active file's language - const handleLanguageChange = (newLanguage: string) => { - setLanguage(newLanguage); - if (activeFileId) { - updateFileLanguage(activeFileId, newLanguage); - } - }; + const language = activeFile?.language ?? ""; + const editorLanguage = language || PLAINTEXT; + const isRunnable = Boolean(language); - // Sync language state with active file's language when file changes useEffect(() => { - if (activeFile?.language) { - setLanguage(activeFile.language); - } else if (activeFile && !activeFile.language) { - // If file has no language, set default to empty (user can select) - setLanguage(""); + if (!activeFileId) return; + + if (!activeFile) { + setError(false); + setOutput(OPEN_FILE_WARNNING); + return; } - }, [activeFile?.id]); + + if (!isRunnable) { + setError(true); + setOutput(DEFAULT_WARNING_UNKNOWN_FILES); + } + }, [activeFileId, activeFile?.name, isRunnable]); // Initialize code snippet when file is first opened useEffect(() => { @@ -63,19 +71,39 @@ export default function CodeEditor() { // Only initialize if file has no content and has a language if (contents[activeFileId] === undefined && activeFile.language) { - const snippet = CODE_SNIPPETS[activeFile.language as keyof typeof CODE_SNIPPETS] ?? ""; + const snippet = + CODE_SNIPPETS[activeFile.language as keyof typeof CODE_SNIPPETS] ?? ""; if (snippet) { updateContent(activeFileId, snippet); } } }, [activeFileId, activeFile?.language]); + useEffect(() => { + if (!projectId) return; + + const loadProject = async () => { + try { + const res = await fetch(`/api/projects/${projectId}`); + if (!res.ok) return; + + const data = await res.json(); + console.log(data); + hydrateFromBackend(data.tree); + } catch (err) { + console.error("Failed to load project", err); + } + }; + + loadProject(); + }, [projectId]); + return (
@@ -98,14 +126,17 @@ export default function CodeEditor() { >
void; + onCreateProject: (data: { + name: string; + track: "WebDevelopment" | "SoftWareDevelopment"; + }) => void; } export function CreateProjectDialog({ onCreateProject, }: CreateProjectDialogProps) { - const { toast } = useToast(); // Toast function - const [open, setOpen] = useState(false); // Dialog state + const { toast } = useToast(); + const [open, setOpen] = useState(false); const [name, setName] = useState(""); - const [type, setType] = useState<"SoftwareDev" | "WebDev">("WebDev"); - const [language, setLanguage] = useState("React"); + const [track, setTrack] = useState<"WebDevelopment" | "SoftWareDevelopment">( + "WebDevelopment" + ); const handleSubmit = () => { - const newProject = { - name, - type, - date: new Date(), - language, - folder: Folder_Example - }; + if (!name.trim()) { + toast({ + title: "Invalid name", + description: "Project name cannot be empty", + variant: "destructive", + }); + return; + } - onCreateProject(newProject); - console.log(newProject); + onCreateProject({ name, track }); - // Show success toast toast({ title: "Project Created", - description: `New project "${name}" added successfully.`, + description: `Project "${name}" created successfully.`, }); - // Close dialog and reset fields setOpen(false); setName(""); - setType("WebDev"); - setLanguage("React"); + setTrack("WebDevelopment"); }; return ( @@ -72,14 +71,15 @@ export function CreateProjectDialog({ Create Project + Create New Project - Fill in the details for your new project. Click create when you're - done. + Provide basic details for your new project. +
+
-
-
- - -
+ + + ); + } + return ( <> - {filteredProjects.length === 0 ? ( - -

- No Recent Projects -

-

- You haven't created any projects yet. Click below to start a new one! -

- -
- ) : ( - filteredProjects.map((project) => ( - - {/* Metallic Shine */} -
-
+ {filteredProjects.map((project) => ( + +
+
-
-

- {project.name} -

-
-
- - {project.type} - - - {project.language} - -
-
- - {new Date(project.date).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - })} - - +

+ {project.name} +

+ +
+
+ + {project.track} + +
+ +
+ + {new Date(project.createdAt).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + })} + + + + - -
+ Open + +
- - )) - )} +
+ + ))} ); }; diff --git a/remote-ide/app/DashBoard/page.tsx b/remote-ide/app/DashBoard/page.tsx index 12a34fd..d70c515 100644 --- a/remote-ide/app/DashBoard/page.tsx +++ b/remote-ide/app/DashBoard/page.tsx @@ -1,29 +1,43 @@ "use client"; -import { useState } from "react"; -import { Search, FileCode2, Command} from "lucide-react"; +import { useEffect, useState } from "react"; +import { Search, FileCode2, Command } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ProjectCard } from "./ProjectCard"; import { SideBar } from "./sidebar"; import Link from "next/link"; +import { useUserStore } from "@/hooks/useUserStore"; import { TProjectCard } from "./types"; -import { projectcard_example } from "@/components/Constants/constants"; -import { useUserdata } from "../context/UserDataContext"; - export default function Dashboard() { const [searchQuery, setSearchQuery] = useState(""); - const { user, setUser } = useUserdata(); - - const [projects, setProjects] = useState(user?.projects || projectcard_example); - + const { user, setUser } = useUserStore(); + // todo + const [projects, setProjects] = useState([]); + + useEffect(() => { + const init = async () => { + try { + const userRes = await fetch("/api/user", { method: "POST" }); + const userData = await userRes.json(); + setUser(userData); + const projectRes = await fetch("/api/projects"); + const projectData = await projectRes.json(); + setProjects(projectData); + } catch (err) { + console.error("Dashboard init failed:", err); + } + }; + + init(); + }, [setUser]); return (
{/* Top Navigation */}
diff --git a/remote-ide/app/WebDev/[id]/layout.tsx b/remote-ide/app/WebEditor/[id]/layout.tsx similarity index 100% rename from remote-ide/app/WebDev/[id]/layout.tsx rename to remote-ide/app/WebEditor/[id]/layout.tsx diff --git a/remote-ide/app/WebDev/[id]/page.tsx b/remote-ide/app/WebEditor/[id]/page.tsx similarity index 96% rename from remote-ide/app/WebDev/[id]/page.tsx rename to remote-ide/app/WebEditor/[id]/page.tsx index 0d9870f..a44dbac 100644 --- a/remote-ide/app/WebDev/[id]/page.tsx +++ b/remote-ide/app/WebEditor/[id]/page.tsx @@ -11,9 +11,7 @@ import { FileExplorer } from "./FileExplorer/FileExplorer"; import { LiveProvider, LiveEditor, LivePreview, LiveError } from "react-live"; import React from "react"; import { default_code } from "@/components/Constants/constants"; -import { FileTree } from "./FileExplorer/types"; -import { useUserdata } from "@/app/context/UserDataContext"; -import { useParams } from "next/navigation"; + export default function WebDev() { diff --git a/remote-ide/app/context/UserDataContext.tsx b/remote-ide/app/context/UserDataContext.tsx deleted file mode 100644 index c4f4dca..0000000 --- a/remote-ide/app/context/UserDataContext.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { createContext, useContext, useState, ReactNode } from "react"; -import { UserType } from "../CodeEditor/[id]/FileExplorer/types"; - -// shape of context value -type UserContextType = { - user: UserType | null; - setUser: (user: UserType) => void; -}; - -// Create the context with default undefined -const UserContext = createContext(undefined); - -// Provider -export const UserProvider = ({ children }: { children: ReactNode }) => { - const [user, setUser] = useState(null); - - return ( - - {children} - - ); -}; - -// Hook to use context -export const useUserdata = () => { - const context = useContext(UserContext); - if (!context) throw new Error("useUser must be used inside UserProvider"); - return context; -}; diff --git a/remote-ide/app/layout.tsx b/remote-ide/app/layout.tsx index 42c45f7..f2bfc0e 100644 --- a/remote-ide/app/layout.tsx +++ b/remote-ide/app/layout.tsx @@ -4,7 +4,6 @@ import { ClerkProvider } from "@clerk/nextjs"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { motion } from "framer-motion"; -import { UserProvider } from "./context/UserDataContext"; // ✅ Make sure path is correct const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,16 +26,14 @@ export default function RootLayout({ - - - {children} - - + + {children} + diff --git a/remote-ide/app/page.tsx b/remote-ide/app/page.tsx index 3981657..205ea9d 100644 --- a/remote-ide/app/page.tsx +++ b/remote-ide/app/page.tsx @@ -2,7 +2,7 @@ import Footer from "@/components/elements/Footer"; import Hero from "@/components/elements/Hero"; -import Hero_4 from "@/components/elements/Hero2"; +import Hero_4 from "@/components/elements/Hero2"; import Hero_3 from "@/components/elements/Hero4"; import Hero_1 from "@/components/elements/Hero5"; import Hero_2 from "@/components/elements/Hero7"; diff --git a/remote-ide/components/Constants/constants.ts b/remote-ide/components/Constants/constants.ts index c3af2dc..896c5e4 100644 --- a/remote-ide/components/Constants/constants.ts +++ b/remote-ide/components/Constants/constants.ts @@ -136,7 +136,7 @@ export const projectcard_example = [ }, { id: "4", - name: "Task Management API", + name: "Task Management api", type: "SoftwareDev", language: "Node.js", date: new Date("2024-03-10"), diff --git a/remote-ide/components/elements/ChatbotRes.tsx b/remote-ide/components/elements/ChatbotRes.tsx index cd4773b..d4cbe6e 100644 --- a/remote-ide/components/elements/ChatbotRes.tsx +++ b/remote-ide/components/elements/ChatbotRes.tsx @@ -1,15 +1,15 @@ import axios from "axios"; -const API_URL = +const api_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent"; -const API_KEY = "AIzaSyBCVruyIS0egMeoZOOMiqbexLHOavEau8c"; // Replace with your actual API key +const api_KEY = "AIzaSyBCVruyIS0egMeoZOOMiqbexLHOavEau8c"; // Replace with your actual api key export const fetchAIResponse = async ({ prompt }: { prompt: string }) => { console.log("in"); try { const response = await axios.post( - `${API_URL}?key=${API_KEY}`, + `${api_URL}?key=${api_KEY}`, { contents: [ { @@ -26,7 +26,23 @@ export const fetchAIResponse = async ({ prompt }: { prompt: string }) => { console.log(response.data); return response.data.candidates[0].content.parts[0].text; - } catch (error) { - return error; + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 429) { + return "Rate limit reached . Please wait a bit and try again."; + } + + const apiMessage = + (error.response?.data as any)?.error?.message || + (error.response?.data as any)?.message; + return `Request failed${status ? ` (${status})` : ""}: ${apiMessage || error.message}`; + } + + if (error instanceof Error) { + return `Error: ${error.message}`; + } + + return "Error: Unknown error occurred."; } }; diff --git a/remote-ide/components/elements/Footer.tsx b/remote-ide/components/elements/Footer.tsx index c91a324..4b237ee 100644 --- a/remote-ide/components/elements/Footer.tsx +++ b/remote-ide/components/elements/Footer.tsx @@ -24,7 +24,7 @@ const Footer = () => { {/* Resources */}

Resources

- {['Documentation', 'API Reference', 'Code Snippets', 'Developer Blog', 'Community Forum'].map((item, i) => ( + {['Documentation', 'api Reference', 'Code Snippets', 'Developer Blog', 'Community Forum'].map((item, i) => (

{item}

))}
diff --git a/remote-ide/components/elements/Hero2.tsx b/remote-ide/components/elements/Hero2.tsx index e746cf3..f6b5d37 100644 --- a/remote-ide/components/elements/Hero2.tsx +++ b/remote-ide/components/elements/Hero2.tsx @@ -41,7 +41,7 @@ const Hero2 = () => { arr.size() - 1; {"\n\n"}   while (left - >= right) {"{\n"} + <= right) {"{\n"}     int mid = left + (right - left) /{" "} 2;{"\n\n"} diff --git a/remote-ide/components/elements/Navbar.tsx b/remote-ide/components/elements/Navbar.tsx index 8da542a..f78c2ef 100644 --- a/remote-ide/components/elements/Navbar.tsx +++ b/remote-ide/components/elements/Navbar.tsx @@ -9,16 +9,16 @@ import Link from "next/link"; import { useClerk, useUser } from "@clerk/nextjs"; import { useState, useEffect } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { useUserdata } from "../../app/context/UserDataContext"; import axios from "axios"; -import { User, User2Icon } from "lucide-react"; +import { User2Icon } from "lucide-react"; +import { useUserStore } from "@/hooks/useUserStore"; export default function Navbar() { const clerk = useClerk(); const [showRedirectPopup, setShowRedirectPopup] = useState(false); const [showWelcomePopup, setShowWelcomePopup] = useState(false); const { user: clerkUser, isSignedIn } = useUser(); - const { user: contextUser, setUser } = useUserdata(); + const { user, setUser, clearUser } = useUserStore(); const handleStartBuilding = (e: React.MouseEvent) => { if (!isSignedIn) { @@ -32,11 +32,10 @@ export default function Navbar() { }; const DBcall = async () => { - console.log("🔥 DBcall triggered"); + console.log("DBcall triggered"); try { if (clerkUser && clerkUser.id) { - const response = await axios.post("/Api/User", { - userId: clerkUser.id, + const response = await axios.post("/api/user", { name: clerkUser.firstName, }); @@ -52,6 +51,7 @@ export default function Navbar() { const logouthandler = async () => { try { + clearUser(); await clerk.signOut(); } catch (error) { console.error("Error signing out:", error); @@ -61,7 +61,7 @@ export default function Navbar() { useEffect(() => { const runEffect = async () => { if (isSignedIn) { - await DBcall(); + await DBcall(); setShowWelcomePopup(true); setTimeout(() => setShowWelcomePopup(false), 1000); } @@ -77,7 +77,7 @@ export default function Navbar() {