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
+}