332 lines
9.9 KiB
TypeScript
Raw Normal View History

2024-04-06 19:03:04 -04:00
"use client"
import Editor, { OnMount } from "@monaco-editor/react"
import monaco from "monaco-editor"
2024-04-26 22:34:56 -04:00
import { useEffect, useRef, useState } from "react"
2024-04-26 00:10:53 -04:00
// import theme from "./theme.json"
2024-04-06 19:03:04 -04:00
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
2024-04-08 23:40:42 -04:00
import {
ChevronLeft,
ChevronRight,
2024-04-27 11:08:19 -04:00
FileJson,
2024-04-30 02:00:50 -04:00
Plus,
2024-04-08 23:40:42 -04:00
RotateCw,
2024-04-30 02:00:50 -04:00
Shell,
SquareTerminal,
2024-04-08 23:40:42 -04:00
TerminalSquare,
} from "lucide-react"
import Tab from "../ui/tab"
2024-04-09 00:50:48 -04:00
import Sidebar from "./sidebar"
2024-04-18 15:32:27 -04:00
import { useClerk } from "@clerk/nextjs"
2024-04-27 11:08:19 -04:00
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
2024-04-06 19:03:04 -04:00
2024-04-26 22:34:56 -04:00
import { io } from "socket.io-client"
2024-04-29 00:50:25 -04:00
import { processFileType, validateName } from "@/lib/utils"
2024-04-27 16:22:35 -04:00
import { toast } from "sonner"
2024-04-28 20:06:47 -04:00
import EditorTerminal from "./terminal"
2024-04-30 02:00:50 -04:00
import { Button } from "../ui/button"
2024-05-01 01:53:49 -04:00
import { User } from "@/lib/types"
2024-04-28 20:06:47 -04:00
2024-04-26 22:34:56 -04:00
export default function CodeEditor({
2024-05-01 01:53:49 -04:00
userData,
2024-04-26 22:34:56 -04:00
sandboxId,
}: {
2024-05-01 01:53:49 -04:00
userData: User
2024-04-26 22:34:56 -04:00
sandboxId: string
}) {
2024-04-28 20:06:47 -04:00
const clerk = useClerk()
2024-04-27 00:20:17 -04:00
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
2024-04-06 19:03:04 -04:00
2024-04-27 00:20:17 -04:00
const handleEditorMount: OnMount = (editor, monaco) => {
editorRef.current = editor
}
2024-04-06 19:03:04 -04:00
2024-04-26 22:34:56 -04:00
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
2024-04-27 00:20:17 -04:00
const [editorLanguage, setEditorLanguage] = useState("plaintext")
const [activeFile, setActiveFile] = useState<string | null>(null)
2024-04-27 11:08:19 -04:00
const [tabs, setTabs] = useState<TTab[]>([])
2024-04-27 00:20:17 -04:00
const [activeId, setActiveId] = useState<string | null>(null)
2024-04-30 02:00:50 -04:00
const [terminals, setTerminals] = useState<string[]>([])
2024-04-26 22:34:56 -04:00
const socket = io(
2024-05-01 01:53:49 -04:00
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxId}`
2024-04-26 22:34:56 -04:00
)
2024-04-27 16:41:25 -04:00
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
const activeTab = tabs.find((t) => t.id === activeId)
2024-04-27 19:12:25 -04:00
console.log("saving:", activeTab?.name, editorRef.current?.getValue())
2024-04-27 16:41:25 -04:00
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: true } : tab
)
)
socket.emit("saveFile", activeId, editorRef.current?.getValue())
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [tabs, activeId])
2024-04-28 20:06:47 -04:00
// WS event handlers:
2024-04-27 16:41:25 -04:00
2024-04-26 22:34:56 -04:00
// connection/disconnection effect
useEffect(() => {
socket.connect()
return () => {
socket.disconnect()
}
}, [])
// event listener effect
useEffect(() => {
2024-04-28 20:06:47 -04:00
const onConnect = () => {}
const onDisconnect = () => {}
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
2024-04-27 00:20:17 -04:00
console.log("onLoadedEvent")
2024-04-26 22:34:56 -04:00
setFiles(files)
}
2024-04-28 20:06:47 -04:00
socket.on("connect", onConnect)
socket.on("disconnect", onDisconnect)
2024-04-26 22:34:56 -04:00
socket.on("loaded", onLoadedEvent)
return () => {
2024-04-28 20:06:47 -04:00
socket.off("connect", onConnect)
socket.off("disconnect", onDisconnect)
2024-04-26 22:34:56 -04:00
socket.off("loaded", onLoadedEvent)
}
}, [])
2024-04-28 20:06:47 -04:00
// Helper functions:
2024-04-06 19:03:04 -04:00
2024-04-27 11:08:19 -04:00
const selectFile = (tab: TTab) => {
2024-04-26 00:10:53 -04:00
setTabs((prev) => {
const exists = prev.find((t) => t.id === tab.id)
if (exists) {
2024-04-27 14:23:09 -04:00
// console.log("exists")
2024-04-26 00:10:53 -04:00
setActiveId(exists.id)
return prev
}
return [...prev, tab]
})
2024-04-27 00:20:17 -04:00
socket.emit("getFile", tab.id, (response: string) => {
setActiveFile(response)
})
2024-04-27 00:28:00 -04:00
setEditorLanguage(processFileType(tab.name))
2024-04-26 00:10:53 -04:00
setActiveId(tab.id)
}
const closeTab = (tab: TFile) => {
const numTabs = tabs.length
const index = tabs.findIndex((t) => t.id === tab.id)
2024-04-30 01:56:43 -04:00
if (index === -1) return
2024-04-27 00:20:17 -04:00
const nextId =
activeId === tab.id
? numTabs === 1
? null
: index < numTabs - 1
? tabs[index + 1].id
: tabs[index - 1].id
: activeId
const nextTab = tabs.find((t) => t.id === nextId)
if (nextTab) selectFile(nextTab)
else setActiveId(null)
2024-04-26 00:10:53 -04:00
setTabs((prev) => prev.filter((t) => t.id !== tab.id))
}
2024-04-27 14:23:09 -04:00
const handleRename = (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => {
2024-04-30 01:56:43 -04:00
if (!validateName(newName, oldName, type)) {
toast.error("Invalid file name.")
console.log("invalid name")
return false
}
2024-04-27 14:23:09 -04:00
2024-04-27 11:08:19 -04:00
socket.emit("renameFile", id, newName)
setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
)
2024-04-27 14:23:09 -04:00
return true
2024-04-27 11:08:19 -04:00
}
2024-04-30 01:56:43 -04:00
const handleDeleteFile = (file: TFile) => {
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response)
})
closeTab(file)
}
const handleDeleteFolder = (folder: TFolder) => {
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
// setFiles(response)
// })
}
2024-04-06 19:03:04 -04:00
return (
<>
2024-04-27 14:23:09 -04:00
<Sidebar
files={files}
selectFile={selectFile}
handleRename={handleRename}
2024-04-30 01:56:43 -04:00
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
2024-04-29 00:50:25 -04:00
socket={socket}
addNew={(name, type) => {
if (type === "file") {
console.log("adding file")
setFiles((prev) => [
...prev,
{ id: `projects/${sandboxId}/${name}`, name, type: "file" },
])
} else {
console.log("adding folder")
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
}
}}
2024-04-27 14:23:09 -04:00
/>
2024-04-06 19:03:04 -04:00
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
className="p-2 flex flex-col"
maxSize={75}
minSize={30}
defaultSize={60}
>
<div className="h-10 w-full flex gap-2">
2024-04-26 00:10:53 -04:00
{tabs.map((tab) => (
<Tab
key={tab.id}
2024-04-27 11:08:19 -04:00
saved={tab.saved}
2024-04-26 00:10:53 -04:00
selected={activeId === tab.id}
2024-04-27 00:20:17 -04:00
onClick={() => {
selectFile(tab)
}}
2024-04-26 00:10:53 -04:00
onClose={() => closeTab(tab)}
>
{tab.name}
</Tab>
))}
2024-04-06 19:03:04 -04:00
</div>
2024-04-08 23:40:42 -04:00
<div className="grow w-full overflow-hidden rounded-md">
2024-04-26 00:10:53 -04:00
{activeId === null ? (
<>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-secondary select-none">
2024-04-27 11:08:19 -04:00
<FileJson className="w-6 h-6 mr-3" />
2024-04-26 00:10:53 -04:00
No file selected.
</div>
</>
) : clerk.loaded ? (
2024-04-18 15:32:27 -04:00
<Editor
height="100%"
2024-04-27 00:20:17 -04:00
// defaultLanguage="typescript"
language={editorLanguage}
onMount={handleEditorMount}
2024-04-27 11:08:19 -04:00
onChange={(value) => {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: false } : tab
)
)
}}
2024-04-18 15:32:27 -04:00
options={{
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
2024-04-27 00:20:17 -04:00
fixedOverflowWidgets: true,
2024-04-18 15:32:27 -04:00
}}
theme="vs-dark"
2024-04-27 00:20:17 -04:00
value={activeFile ?? ""}
2024-04-18 15:32:27 -04:00
/>
) : null}
2024-04-06 19:03:04 -04:00
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
2024-04-08 23:40:42 -04:00
<div className="h-10 select-none w-full flex gap-2">
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
Preview
<div className="flex space-x-1 translate-x-1">
2024-04-09 00:50:48 -04:00
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm">
2024-04-08 23:40:42 -04:00
<TerminalSquare className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm">
2024-04-08 23:40:42 -04:00
<ChevronLeft className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm">
2024-04-08 23:40:42 -04:00
<ChevronRight className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm">
2024-04-08 23:40:42 -04:00
<RotateCw className="w-3 h-3" />
</div>
</div>
</div>
2024-04-06 19:03:04 -04:00
</div>
2024-04-08 23:40:42 -04:00
<div className="w-full grow rounded-md bg-foreground"></div>
2024-04-06 19:03:04 -04:00
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
2024-04-29 21:36:33 -04:00
<div className="h-10 w-full flex gap-2 shrink-0">
2024-04-30 02:00:50 -04:00
<Tab selected>
<SquareTerminal className="w-4 h-4 mr-2" />
Shell
</Tab>
<Button
size="smIcon"
variant={"secondary"}
className={`font-normal select-none text-muted-foreground`}
>
<Plus className="w-4 h-4" />
</Button>
2024-04-06 19:03:04 -04:00
</div>
2024-04-29 21:36:33 -04:00
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
2024-04-28 20:06:47 -04:00
{socket ? <EditorTerminal socket={socket} /> : null}
</div>
2024-04-06 19:03:04 -04:00
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</>
)
}