diff --git a/frontend/app/(app)/code/[id]/page.tsx b/frontend/app/(app)/code/[id]/page.tsx index 15c9f04..b22810f 100644 --- a/frontend/app/(app)/code/[id]/page.tsx +++ b/frontend/app/(app)/code/[id]/page.tsx @@ -1,28 +1,87 @@ import Navbar from "@/components/editor/navbar" -import { User } from "@/lib/types" +import { TFile, TFolder } from "@/components/editor/sidebar/types" +import { R2Files, User } from "@/lib/types" import { currentUser } from "@clerk/nextjs" import dynamic from "next/dynamic" -import { redirect } from "next/navigation" +import { notFound, redirect } from "next/navigation" const CodeEditor = dynamic(() => import("@/components/editor"), { ssr: false, }) -export default async function CodePage() { +const getUserData = async (id: string) => { + const userRes = await fetch(`http://localhost:8787/api/user?id=${id}`) + const userData: User = await userRes.json() + return userData +} + +const getSandboxFiles = async (id: string) => { + const sandboxRes = await fetch( + `https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` + ) + const sandboxData: R2Files = await sandboxRes.json() + + if (sandboxData.objects.length === 0) return notFound() + const paths = sandboxData.objects.map((obj) => obj.key) + return processFiles(paths, id) +} + +const processFiles = (paths: string[], id: string): (TFile | TFolder)[] => { + const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } + + paths.forEach((path) => { + const allParts = path.split("/") + if (allParts[1] !== id) return notFound() + + const parts = allParts.slice(2) + let current: TFolder = root + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const isFile = i === parts.length - 1 && part.includes(".") + const existing = current.children.find((child) => child.name === part) + + if (existing) { + if (!isFile) { + current = existing as TFolder + } + } else { + if (isFile) { + const file: TFile = { id: path, type: "file", name: part } + current.children.push(file) + } else { + const folder: TFolder = { + id: path, + type: "folder", + name: part, + children: [], + } + current.children.push(folder) + current = folder + } + } + } + }) + + return root.children +} + +export default async function CodePage({ params }: { params: { id: string } }) { const user = await currentUser() + const sandboxId = params.id if (!user) { redirect("/") } - const userRes = await fetch(`http://localhost:8787/api/user?id=${user.id}`) - const userData = (await userRes.json()) as User + const userData = await getUserData(user.id) + const sandboxFiles = await getSandboxFiles(sandboxId) return (
- +
) diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 4e54482..8885e76 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -3,55 +3,26 @@ import Editor, { OnMount } from "@monaco-editor/react" import monaco from "monaco-editor" import { useRef, useState } from "react" -import theme from "./theme.json" +// import theme from "./theme.json" import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable" -import { Button } from "../ui/button" import { ChevronLeft, ChevronRight, - RotateCcw, RotateCw, - Terminal, TerminalSquare, - X, } from "lucide-react" import Tab from "../ui/tab" import Sidebar from "./sidebar" import { useClerk } from "@clerk/nextjs" +import { TFile, TFolder } from "./sidebar/types" -export default function CodeEditor() { - const editorRef = useRef(null) - const [code, setCode] = useState([ - { - language: "css", - name: "style.css", - value: `body { background-color: #282c34; color: white; }`, - }, - { - language: "html", - name: "index.html", - value: ` - - - - - -

Hello, world!

- - -`, - }, - { - language: "javascript", - name: "script.js", - value: `console.log("Hello, world!")`, - }, - ]) +export default function CodeEditor({ files }: { files: (TFile | TFolder)[] }) { + // const editorRef = useRef(null) // const handleEditorMount: OnMount = (editor, monaco) => { // editorRef.current = editor @@ -59,9 +30,42 @@ export default function CodeEditor() { const clerk = useClerk() + const [tabs, setTabs] = useState([]) + const [activeId, setActiveId] = useState(null) + + const selectFile = (tab: TFile) => { + setTabs((prev) => { + const exists = prev.find((t) => t.id === tab.id) + if (exists) { + setActiveId(exists.id) + return prev + } + return [...prev, tab] + }) + setActiveId(tab.id) + } + + const closeTab = (tab: TFile) => { + const numTabs = tabs.length + const index = tabs.findIndex((t) => t.id === tab.id) + setActiveId((prev) => { + const next = + prev === tab.id + ? numTabs === 1 + ? null + : index < numTabs - 1 + ? tabs[index + 1].id + : tabs[index - 1].id + : prev + + return next + }) + setTabs((prev) => prev.filter((t) => t.id !== tab.id)) + } + return ( <> - +
- index.html - style.css + {tabs.map((tab) => ( + setActiveId(tab.id)} + onClose={() => closeTab(tab)} + > + {tab.name} + + ))}
- {clerk.loaded ? ( + {activeId === null ? ( + <> +
+ No file selected. +
+ + ) : clerk.loaded ? ( void +}) { + const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`) -export default function SidebarFile({ data }: { data: TFile }) { return ( -
+
+ ) } diff --git a/frontend/components/editor/sidebar/folder.tsx b/frontend/components/editor/sidebar/folder.tsx index fdb764e..d4bdc74 100644 --- a/frontend/components/editor/sidebar/folder.tsx +++ b/frontend/components/editor/sidebar/folder.tsx @@ -3,10 +3,16 @@ import Image from "next/image" import { useState } from "react" import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js" -import { TFolder } from "./types" +import { TFile, TFolder } from "./types" import SidebarFile from "./file" -export default function SidebarFolder({ data }: { data: TFolder }) { +export default function SidebarFolder({ + data, + selectFile, +}: { + data: TFolder + selectFile: (file: TFile) => void +}) { const [isOpen, setIsOpen] = useState(false) const folder = isOpen ? getIconForOpenFolder(data.name) @@ -33,9 +39,17 @@ export default function SidebarFolder({ data }: { data: TFolder }) {
{data.children.map((child) => child.type === "file" ? ( - + ) : ( - + ) )}
diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index 108ca01..6e51fea 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -1,72 +1,26 @@ +"use client" + import { FilePlus, FolderPlus, Search } from "lucide-react" import SidebarFile from "./file" import SidebarFolder from "./folder" import { TFile, TFolder } from "./types" -const data: (TFile | TFolder)[] = [ - { - id: "index.tsx", - type: "file", - name: "index.tsx", - }, - { - id: "components", - type: "folder", - name: "components", - children: [ - { - id: "navbar.tsx", - type: "file", - name: "navbar.tsx", - }, - { - id: "ui", - type: "folder", - name: "ui", - children: [ - { - id: "Button.tsx", - type: "file", - name: "Button.tsx", - }, - { - id: "Input.tsx", - type: "file", - name: "Input.tsx", - }, - ], - }, - ], - }, - { - id: "App.tsx", - type: "file", - name: "App.tsx", - }, - { - id: "styles", - type: "folder", - name: "styles", - children: [ - { - id: "style.css", - type: "file", - name: "style.css", - }, - { - id: "index.css", - type: "file", - name: "index.css", - }, - ], - }, -] +// Note: add renaming validation: +// In general: must not contain / or \ or whitespace, not empty, no duplicates +// Files: must contain dot +// Folders: must not contain dot -export default function Sidebar() { +export default function Sidebar({ + files, + selectFile, +}: { + files: (TFile | TFolder)[] + selectFile: (tab: TFile) => void +}) { return (
-
EXPLORER
+
Explorer
@@ -80,13 +34,15 @@ export default function Sidebar() {
- {/* - */} - {data.map((child) => + {files.map((child) => child.type === "file" ? ( - + ) : ( - + ) )}
diff --git a/frontend/components/ui/tab.tsx b/frontend/components/ui/tab.tsx index 68ae607..1c42244 100644 --- a/frontend/components/ui/tab.tsx +++ b/frontend/components/ui/tab.tsx @@ -2,6 +2,7 @@ import { X } from "lucide-react" import { Button } from "./button" +import { useEffect } from "react" export default function Tab({ children, @@ -19,13 +20,23 @@ export default function Tab({ onClick={onClick ?? undefined} size="sm" variant={"secondary"} - className={`group select-none ${ - selected ? "bg-neutral-700 hover:bg-neutral-600" : "" + className={`group font-normal select-none ${ + selected + ? "bg-neutral-700 hover:bg-neutral-600 text-foreground" + : "text-muted-foreground" }`} > {children}
{ + e.stopPropagation() + e.preventDefault() + onClose() + } + : undefined + } className="h-5 w-5 ml-0.5 flex items-center justify-center translate-x-1 transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm" > diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index fc2851e..bc993be 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -15,3 +15,20 @@ export type Sandbox = { bucket: string | null userId: string } + +export type R2Files = { + objects: R2FileData[] + truncated: boolean + delimitedPrefixes: any[] +} + +export type R2FileData = { + storageClass: string + uploaded: string + checksums: any + httpEtag: string + etag: string + size: number + version: string + key: string +}