refactor terminal logic + state variables. temporarily break terminal

This commit is contained in:
Ishaan Dey 2024-05-06 22:59:49 -07:00
parent 84c49f0d9d
commit 4e42555887
7 changed files with 421 additions and 351 deletions

View File

@ -223,6 +223,7 @@ io.on("connection", async (socket) => {
}) })
socket.on("createTerminal", ({ id }: { id: string }) => { socket.on("createTerminal", ({ id }: { id: string }) => {
console.log("creating terminal", id)
if (terminals[id]) { if (terminals[id]) {
console.log("Terminal already exists.") console.log("Terminal already exists.")
return return
@ -239,6 +240,7 @@ io.on("connection", async (socket) => {
}) })
const onData = pty.onData((data) => { const onData = pty.onData((data) => {
console.log("ondata")
socket.emit("terminalResponse", { socket.emit("terminalResponse", {
// data: Buffer.from(data, "utf-8").toString("base64"), // data: Buffer.from(data, "utf-8").toString("base64"),
data, data,
@ -315,11 +317,11 @@ io.on("connection", async (socket) => {
delete terminals[t[0]] delete terminals[t[0]]
}) })
console.log("The owner disconnected.") // console.log("The owner disconnected.")
socket.broadcast.emit("ownerDisconnected") socket.broadcast.emit("ownerDisconnected")
} }
else { else {
console.log("A shared user disconnected.") // console.log("A shared user disconnected.")
} }
const sockets = await io.fetchSockets() const sockets = await io.fetchSockets()

View File

@ -1,64 +1,83 @@
import Navbar from "@/components/editor/navbar" import Navbar from "@/components/editor/navbar";
import { Room } from "@/components/editor/live/room" import { Room } from "@/components/editor/live/room";
import { Sandbox, User, UsersToSandboxes } from "@/lib/types" import { Sandbox, User, UsersToSandboxes } from "@/lib/types";
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs";
import dynamic from "next/dynamic" import dynamic from "next/dynamic";
import { redirect } from "next/navigation" import { notFound, redirect } from "next/navigation";
import Loading from "@/components/editor/loading";
import { Suspense } from "react";
const CodeEditor = dynamic(() => import("@/components/editor"), { const CodeEditor = dynamic(() => import("@/components/editor"), {
ssr: false, ssr: false,
}) });
const getUserData = async (id: string) => { const getUserData = async (id: string) => {
const userRes = await fetch( const userRes = await fetch(
`https://database.ishaan1013.workers.dev/api/user?id=${id}` `https://database.ishaan1013.workers.dev/api/user?id=${id}`
) );
const userData: User = await userRes.json() const userData: User = await userRes.json();
return userData return userData;
} };
const getSandboxData = async (id: string) => { const getSandboxData = async (id: string) => {
const sandboxRes = await fetch( const sandboxRes = await fetch(
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}` `https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
) );
const sandboxData: Sandbox = await sandboxRes.json() const sandboxData: Sandbox = await sandboxRes.json();
return sandboxData return sandboxData;
} };
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => { const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
const shared = await Promise.all( const shared = await Promise.all(
usersToSandboxes.map(async (user) => { usersToSandboxes.map(async (user) => {
const userRes = await fetch( const userRes = await fetch(
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}` `https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
) );
const userData: User = await userRes.json() const userData: User = await userRes.json();
return { id: userData.id, name: userData.name } return { id: userData.id, name: userData.name };
}) })
) );
return shared return shared;
} };
export default async function CodePage({ params }: { params: { id: string } }) { export default async function CodePage({ params }: { params: { id: string } }) {
const user = await currentUser() const user = await currentUser();
const sandboxId = params.id const sandboxId = params.id;
if (!user) { if (!user) {
redirect("/") redirect("/");
} }
const userData = await getUserData(user.id) const userData = await getUserData(user.id);
const sandboxData = await getSandboxData(sandboxId) const sandboxData = await getSandboxData(sandboxId);
const shared = await getSharedUsers(sandboxData.usersToSandboxes) const shared = await getSharedUsers(sandboxData.usersToSandboxes);
const isOwner = sandboxData.userId === user.id;
const isSharedUser = shared.some((uts) => uts.id === user.id);
if (!isOwner && !isSharedUser) {
return notFound();
}
return ( return (
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}> <Suspense fallback={<Loading />}>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} /> <Room id={sandboxId}>
<div className="w-screen flex grow"> <Navbar
<CodeEditor userData={userData} sandboxData={sandboxData} /> userData={userData}
</div> sandboxData={sandboxData}
</Room> shared={shared}
/>
<div className="w-screen flex grow">
<CodeEditor
userData={userData}
sandboxData={sandboxData}
isSharedUser={isSharedUser}
/>
</div>
</Room>
</Suspense>
</div> </div>
) );
} }

View File

@ -1,23 +1,23 @@
"use client" "use client";
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react";
import monaco from "monaco-editor" import monaco from "monaco-editor";
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react";
import { io } from "socket.io-client" import { io } from "socket.io-client";
import { toast } from "sonner" import { toast } from "sonner";
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs";
import * as Y from "yjs" import * as Y from "yjs";
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs";
import { MonacoBinding } from "y-monaco" import { MonacoBinding } from "y-monaco";
import { Awareness } from "y-protocols/awareness" import { Awareness } from "y-protocols/awareness";
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config" import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config";
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable";
import { import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@ -28,56 +28,66 @@ import {
Shell, Shell,
SquareTerminal, SquareTerminal,
TerminalSquare, TerminalSquare,
} from "lucide-react" } from "lucide-react";
import Tab from "../ui/tab" import Tab from "../ui/tab";
import Sidebar from "./sidebar" import Sidebar from "./sidebar";
import EditorTerminal from "./terminal" import EditorTerminal from "./terminal";
import { Button } from "../ui/button" import { Button } from "../ui/button";
import GenerateInput from "./generate" import GenerateInput from "./generate";
import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types" import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types";
import { processFileType, validateName } from "@/lib/utils" import { processFileType, validateName } from "@/lib/utils";
import { Cursors } from "./live/cursors" import { Cursors } from "./live/cursors";
import { Terminal } from "@xterm/xterm";
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
sandboxData, sandboxData,
isSharedUser,
}: { }: {
userData: User userData: User;
sandboxData: Sandbox sandboxData: Sandbox;
isSharedUser: boolean;
}) { }) {
const [files, setFiles] = useState<(TFolder | TFile)[]>([]) const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
const [tabs, setTabs] = useState<TTab[]>([]) const [tabs, setTabs] = useState<TTab[]>([]);
const [editorLanguage, setEditorLanguage] = useState("plaintext") const [editorLanguage, setEditorLanguage] = useState("plaintext");
const [activeId, setActiveId] = useState<string>("") const [activeFileId, setActiveFileId] = useState<string>("");
const [activeFile, setActiveFile] = useState<string | null>(null) const [activeFileContent, setActiveFileContent] = useState<string | null>(
const [cursorLine, setCursorLine] = useState(0) null
);
const [cursorLine, setCursorLine] = useState(0);
const [generate, setGenerate] = useState<{ const [generate, setGenerate] = useState<{
show: boolean show: boolean;
id: string id: string;
line: number line: number;
widget: monaco.editor.IContentWidget | undefined widget: monaco.editor.IContentWidget | undefined;
pref: monaco.editor.ContentWidgetPositionPreference[] pref: monaco.editor.ContentWidgetPositionPreference[];
width: number width: number;
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 }) }>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 });
const [decorations, setDecorations] = useState<{ const [decorations, setDecorations] = useState<{
options: monaco.editor.IModelDeltaDecoration[] options: monaco.editor.IModelDeltaDecoration[];
instance: monaco.editor.IEditorDecorationsCollection | undefined instance: monaco.editor.IEditorDecorationsCollection | undefined;
}>({ options: [], instance: undefined }) }>({ options: [], instance: undefined });
const [terminals, setTerminals] = useState<string[]>([]) const [terminals, setTerminals] = useState<
const [provider, setProvider] = useState<TypedLiveblocksProvider>() {
const [ai, setAi] = useState(false) id: string;
terminal: Terminal | null;
}[]
>([]);
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
const [ai, setAi] = useState(false);
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id;
const clerk = useClerk() const clerk = useClerk();
const room = useRoom() const room = useRoom();
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null) // const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const [editorRef, setEditorRef] = const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>() useState<monaco.editor.IStandaloneCodeEditor>();
const editorContainerRef = useRef<HTMLDivElement>(null) const editorContainerRef = useRef<HTMLDivElement>(null);
const monacoRef = useRef<typeof monaco | null>(null) const monacoRef = useRef<typeof monaco | null>(null);
const generateRef = useRef<HTMLDivElement>(null) const generateRef = useRef<HTMLDivElement>(null);
const generateWidgetRef = useRef<HTMLDivElement>(null) const generateWidgetRef = useRef<HTMLDivElement>(null);
const handleEditorWillMount: BeforeMount = (monaco) => { const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([ monaco.editor.addKeybindingRules([
@ -86,20 +96,20 @@ export default function CodeEditor({
command: "null", command: "null",
// when: "textInputFocus", // when: "textInputFocus",
}, },
]) ]);
} };
const handleEditorMount: OnMount = (editor, monaco) => { const handleEditorMount: OnMount = (editor, monaco) => {
setEditorRef(editor) setEditorRef(editor);
monacoRef.current = monaco monacoRef.current = monaco;
editor.onDidChangeCursorPosition((e) => { editor.onDidChangeCursorPosition((e) => {
const { column, lineNumber } = e.position const { column, lineNumber } = e.position;
if (lineNumber === cursorLine) return if (lineNumber === cursorLine) return;
setCursorLine(lineNumber) setCursorLine(lineNumber);
const model = editor.getModel() const model = editor.getModel();
const endColumn = model?.getLineContent(lineNumber).length || 0 const endColumn = model?.getLineContent(lineNumber).length || 0;
setDecorations((prev) => { setDecorations((prev) => {
return { return {
@ -117,18 +127,18 @@ export default function CodeEditor({
}, },
}, },
], ],
} };
}) });
}) });
editor.onDidBlurEditorText((e) => { editor.onDidBlurEditorText((e) => {
setDecorations((prev) => { setDecorations((prev) => {
return { return {
...prev, ...prev,
options: [], options: [],
} };
}) });
}) });
editor.addAction({ editor.addAction({
id: "generate", id: "generate",
@ -142,11 +152,11 @@ export default function CodeEditor({
...prev, ...prev,
show: !prev.show, show: !prev.show,
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW], pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
} };
}) });
}, },
}) });
} };
useEffect(() => { useEffect(() => {
if (!ai) { if (!ai) {
@ -154,32 +164,32 @@ export default function CodeEditor({
return { return {
...prev, ...prev,
show: false, show: false,
} };
}) });
return return;
} }
if (generate.show) { if (generate.show) {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return if (!generateRef.current) return;
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 3, heightInLines: 3,
domNode: generateRef.current, domNode: generateRef.current,
}) });
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id, line: cursorLine } return { ...prev, id, line: cursorLine };
}) });
}) });
if (!generateWidgetRef.current) return if (!generateWidgetRef.current) return;
const widgetElement = generateWidgetRef.current const widgetElement = generateWidgetRef.current;
const contentWidget = { const contentWidget = {
getDomNode: () => { getDomNode: () => {
return widgetElement return widgetElement;
}, },
getId: () => { getId: () => {
return "generate.widget" return "generate.widget";
}, },
getPosition: () => { getPosition: () => {
return { return {
@ -188,232 +198,253 @@ export default function CodeEditor({
column: 1, column: 1,
}, },
preference: generate.pref, preference: generate.pref,
} };
}, },
} };
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, widget: contentWidget } return { ...prev, widget: contentWidget };
}) });
editorRef?.addContentWidget(contentWidget) editorRef?.addContentWidget(contentWidget);
if (generateRef.current && generateWidgetRef.current) { if (generateRef.current && generateWidgetRef.current) {
editorRef?.applyFontInfo(generateRef.current) editorRef?.applyFontInfo(generateRef.current);
editorRef?.applyFontInfo(generateWidgetRef.current) editorRef?.applyFontInfo(generateWidgetRef.current);
} }
} else { } else {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id) changeAccessor.removeZone(generate.id);
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id: "" } return { ...prev, id: "" };
}) });
}) });
if (!generate.widget) return if (!generate.widget) return;
editorRef?.removeContentWidget(generate.widget) editorRef?.removeContentWidget(generate.widget);
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
widget: undefined, widget: undefined,
} };
}) });
} }
}, [generate.show]) }, [generate.show]);
useEffect(() => { useEffect(() => {
if (decorations.options.length === 0) { if (decorations.options.length === 0) {
decorations.instance?.clear() decorations.instance?.clear();
} }
if (!ai) return if (!ai) return;
if (decorations.instance) { if (decorations.instance) {
decorations.instance.set(decorations.options) decorations.instance.set(decorations.options);
} else { } else {
const instance = editorRef?.createDecorationsCollection() const instance = editorRef?.createDecorationsCollection();
instance?.set(decorations.options) instance?.set(decorations.options);
setDecorations((prev) => { setDecorations((prev) => {
return { return {
...prev, ...prev,
instance, instance,
} };
}) });
} }
}, [decorations.options]) }, [decorations.options]);
const socket = io( const socket = io(
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
) );
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault();
// const activeTab = tabs.find((t) => t.id === activeId) // const activeTab = tabs.find((t) => t.id === activeFileId)
// console.log("saving:", activeTab?.name, editorRef?.getValue()) // console.log("saving:", activeTab?.name, editorRef?.getValue())
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
) );
socket.emit("saveFile", activeId, editorRef?.getValue()) socket.emit("saveFile", activeFileId, editorRef?.getValue());
} }
} };
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => { return () => {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down);
} };
}, [tabs, activeId]) }, [tabs, activeFileId]);
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
const { width } = entry.contentRect const { width } = entry.contentRect;
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, width } return { ...prev, width };
}) });
} }
}) });
useEffect(() => { useEffect(() => {
const tab = tabs.find((t) => t.id === activeId) const tab = tabs.find((t) => t.id === activeFileId);
const model = editorRef?.getModel() const model = editorRef?.getModel();
if (!editorRef || !tab || !model) return if (!editorRef || !tab || !model) return;
const yDoc = new Y.Doc() const yDoc = new Y.Doc();
const yText = yDoc.getText(tab.id) const yText = yDoc.getText(tab.id);
const yProvider: any = new LiveblocksProvider(room, yDoc) const yProvider: any = new LiveblocksProvider(room, yDoc);
const onSync = (isSynced: boolean) => { const onSync = (isSynced: boolean) => {
if (isSynced) { if (isSynced) {
const text = yText.toString() const text = yText.toString();
if (text === "") { if (text === "") {
if (activeFile) { if (activeFileContent) {
yText.insert(0, activeFile) yText.insert(0, activeFileContent);
} else { } else {
setTimeout(() => { setTimeout(() => {
yText.insert(0, editorRef.getValue()) yText.insert(0, editorRef.getValue());
}, 0) }, 0);
} }
} }
} else { } else {
// Yjs content is not synchronized // Yjs content is not synchronized
} }
} };
yProvider.on("sync", onSync) yProvider.on("sync", onSync);
setProvider(yProvider) setProvider(yProvider);
const binding = new MonacoBinding( const binding = new MonacoBinding(
yText, yText,
model, model,
new Set([editorRef]), new Set([editorRef]),
yProvider.awareness as Awareness yProvider.awareness as Awareness
) );
return () => { return () => {
yDoc.destroy() yDoc.destroy();
yProvider.destroy() yProvider.destroy();
binding.destroy() binding.destroy();
yProvider.off("sync", onSync) yProvider.off("sync", onSync);
} };
}, [editorRef, room, activeFile]) }, [editorRef, room, activeFileContent]);
// connection/disconnection effect + resizeobserver // connection/disconnection effect + resizeobserver
useEffect(() => { useEffect(() => {
socket.connect() socket.connect();
if (editorContainerRef.current) { if (editorContainerRef.current) {
resizeObserver.observe(editorContainerRef.current) resizeObserver.observe(editorContainerRef.current);
} }
return () => { return () => {
socket.disconnect() socket.disconnect();
resizeObserver.disconnect() resizeObserver.disconnect();
} };
}, []) }, []);
// event listener effect // event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => {} const onConnect = () => {
console.log("connected");
setTimeout(() => {
socket.emit("createTerminal", { id: "testId" });
}, 1000);
};
const onDisconnect = () => {} const onDisconnect = () => {};
const onLoadedEvent = (files: (TFolder | TFile)[]) => { const onLoadedEvent = (files: (TFolder | TFile)[]) => {
setFiles(files) setFiles(files);
} };
const onRateLimit = (message: string) => { const onRateLimit = (message: string) => {
toast.error(message) toast.error(message);
} };
socket.on("connect", onConnect) const onTerminalResponse = (response: { id: string; data: string }) => {
socket.on("disconnect", onDisconnect) const res = response.data;
socket.on("loaded", onLoadedEvent) console.log("terminal response:", res);
socket.on("rateLimit", onRateLimit)
const term = terminals.find((t) => t.id === response.id);
if (term && term.terminal) term.terminal.write(res);
};
socket.on("connect", onConnect);
socket.on("disconnect", onDisconnect);
socket.on("loaded", onLoadedEvent);
socket.on("rateLimit", onRateLimit);
socket.on("terminalResponse", onTerminalResponse);
return () => { return () => {
socket.off("connect", onConnect) socket.off("connect", onConnect);
socket.off("disconnect", onDisconnect) socket.off("disconnect", onDisconnect);
socket.off("loaded", onLoadedEvent) socket.off("loaded", onLoadedEvent);
socket.off("rateLimit", onRateLimit) socket.off("rateLimit", onRateLimit);
} socket.off("terminalResponse", onTerminalResponse);
}, []) };
}, []);
// Helper functions: // Helper functions:
const createTerminal = () => {
const id = "testId";
socket.emit("createTerminal", { id });
};
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeId) return if (tab.id === activeFileId) return;
const exists = tabs.find((t) => t.id === tab.id) const exists = tabs.find((t) => t.id === tab.id);
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
setActiveId(exists.id) setActiveFileId(exists.id);
return prev return prev;
} }
return [...prev, tab] return [...prev, tab];
}) });
socket.emit("getFile", tab.id, (response: string) => { socket.emit("getFile", tab.id, (response: string) => {
setActiveFile(response) setActiveFileContent(response);
}) });
setEditorLanguage(processFileType(tab.name)) setEditorLanguage(processFileType(tab.name));
setActiveId(tab.id) setActiveFileId(tab.id);
} };
const closeTab = (tab: TFile) => { const closeTab = (tab: TFile) => {
const numTabs = tabs.length const numTabs = tabs.length;
const index = tabs.findIndex((t) => t.id === tab.id) const index = tabs.findIndex((t) => t.id === tab.id);
if (index === -1) return if (index === -1) return;
const nextId = const nextId =
activeId === tab.id activeFileId === tab.id
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeId : activeFileId;
setTabs((prev) => prev.filter((t) => t.id !== tab.id)) setTabs((prev) => prev.filter((t) => t.id !== tab.id));
if (!nextId) { if (!nextId) {
setActiveId("") setActiveFileId("");
} else { } else {
const nextTab = tabs.find((t) => t.id === nextId) const nextTab = tabs.find((t) => t.id === nextId);
if (nextTab) { if (nextTab) {
selectFile(nextTab) selectFile(nextTab);
} }
} }
} };
const handleRename = ( const handleRename = (
id: string, id: string,
@ -421,32 +452,32 @@ export default function CodeEditor({
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => { ) => {
const valid = validateName(newName, oldName, type) const valid = validateName(newName, oldName, type);
if (!valid.status) { if (!valid.status) {
if (valid.message) toast.error("Invalid file name.") if (valid.message) toast.error("Invalid file name.");
return false return false;
} }
socket.emit("renameFile", id, newName) socket.emit("renameFile", id, newName);
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
) );
return true return true;
} };
const handleDeleteFile = (file: TFile) => { const handleDeleteFile = (file: TFile) => {
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response);
}) });
closeTab(file) closeTab(file);
} };
const handleDeleteFolder = (folder: TFolder) => { const handleDeleteFolder = (folder: TFolder) => {
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { // socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
// setFiles(response) // setFiles(response)
// }) // })
} };
return ( return (
<> <>
@ -458,7 +489,7 @@ export default function CodeEditor({
socket={socket} socket={socket}
width={generate.width - 90} width={generate.width - 90}
data={{ data={{
fileName: tabs.find((t) => t.id === activeId)?.name ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "", code: editorRef?.getValue() ?? "",
line: generate.line, line: generate.line,
}} }}
@ -467,33 +498,33 @@ export default function CodeEditor({
}} }}
onExpand={() => { onExpand={() => {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id) changeAccessor.removeZone(generate.id);
if (!generateRef.current) return if (!generateRef.current) return;
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 12, heightInLines: 12,
domNode: generateRef.current, domNode: generateRef.current,
}) });
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id } return { ...prev, id };
}) });
}) });
}} }}
onAccept={(code: string) => { onAccept={(code: string) => {
const line = generate.line const line = generate.line;
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
show: !prev.show, show: !prev.show,
} };
}) });
const file = editorRef?.getValue() const file = editorRef?.getValue();
const lines = file?.split("\n") || [] const lines = file?.split("\n") || [];
lines.splice(line - 1, 0, code) lines.splice(line - 1, 0, code);
const updatedFile = lines.join("\n") const updatedFile = lines.join("\n");
editorRef?.setValue(updatedFile) editorRef?.setValue(updatedFile);
}} }}
/> />
) : null} ) : null}
@ -511,9 +542,9 @@ export default function CodeEditor({
setFiles((prev) => [ setFiles((prev) => [
...prev, ...prev,
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" }, { id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
]) ]);
} else { } else {
console.log("adding folder") console.log("adding folder");
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }]) // setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
} }
}} }}
@ -532,9 +563,9 @@ export default function CodeEditor({
<Tab <Tab
key={tab.id} key={tab.id}
saved={tab.saved} saved={tab.saved}
selected={activeId === tab.id} selected={activeFileId === tab.id}
onClick={(e) => { onClick={(e) => {
selectFile(tab) selectFile(tab);
}} }}
onClose={() => closeTab(tab)} onClose={() => closeTab(tab)}
> >
@ -546,7 +577,7 @@ export default function CodeEditor({
ref={editorContainerRef} ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md relative" className="grow w-full overflow-hidden rounded-md relative"
> >
{!activeId ? ( {!activeFileId ? (
<> <>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<FileJson className="w-6 h-6 mr-3" /> <FileJson className="w-6 h-6 mr-3" />
@ -562,18 +593,22 @@ export default function CodeEditor({
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
if (value === activeFile) { if (value === activeFileContent) {
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: true } : tab tab.id === activeFileId
? { ...tab, saved: true }
: tab
) )
) );
} else { } else {
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: false } : tab tab.id === activeFileId
? { ...tab, saved: false }
: tab
) )
) );
} }
}} }}
options={{ options={{
@ -589,7 +624,7 @@ export default function CodeEditor({
fontFamily: "var(--font-geist-mono)", fontFamily: "var(--font-geist-mono)",
}} }}
theme="vs-dark" theme="vs-dark"
value={activeFile ?? ""} value={activeFileContent ?? ""}
/> />
</> </>
) : ( ) : (
@ -645,7 +680,9 @@ export default function CodeEditor({
<Button <Button
onClick={() => { onClick={() => {
if (terminals.length >= 4) { if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.") toast.error(
"You reached the maximum # of terminals."
);
} }
}} }}
size="smIcon" size="smIcon"
@ -656,7 +693,9 @@ export default function CodeEditor({
</Button> </Button>
</div> </div>
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary"> <div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
{socket ? <EditorTerminal socket={socket} /> : null} {/* {socket ? <EditorTerminal socket={socket} term={
} /> : null} */}
</div> </div>
</> </>
) : ( ) : (
@ -670,5 +709,5 @@ export default function CodeEditor({
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</> </>
) );
} }

View File

@ -1,7 +1,7 @@
import Image from "next/image" import Image from "next/image";
import Logo from "@/assets/logo.svg" import Logo from "@/assets/logo.svg";
import { Skeleton } from "../ui/skeleton" import { Skeleton } from "../ui/skeleton";
import { Loader, Loader2 } from "lucide-react" import { Loader, Loader2 } from "lucide-react";
export default function Loading() { export default function Loading() {
return ( return (
@ -31,11 +31,15 @@ export default function Loading() {
</div> </div>
</div> </div>
</div> </div>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-secondary select-none"> <div className="w-full h-full grid grid-cols-5 grid-rows-2 gap-4 p-2">
<Loader2 className="w-6 h-6 mr-3 animate-spin" /> <div className="w-full h-full col-span-3 row-span-2 flex items-center justify-center text-xl font-medium text-secondary select-none">
Loading... <Loader2 className="w-6 h-6 mr-3 animate-spin" />
</div>{" "} Loading...
</div>
<Skeleton className="w-full h-full col-span-2 rounded-md" />
<Skeleton className="w-full h-full col-span-2 rounded-md" />
</div>
</div> </div>
</div> </div>
) );
} }

View File

@ -35,6 +35,7 @@ import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { deleteSandbox, updateSandbox } from "@/lib/actions" import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { toast } from "sonner"
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1).max(16), name: z.string().min(1).max(16),
@ -69,6 +70,8 @@ export default function EditSandboxModal({
setLoading(true) setLoading(true)
await updateSandbox({ id: data.id, ...values }) await updateSandbox({ id: data.id, ...values })
toast.success("Sandbox updated successfully")
setLoading(false) setLoading(false)
} }

View File

@ -18,7 +18,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Loader2, UserPlus, X } from "lucide-react" import { Link, Loader2, UserPlus, X } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -75,41 +75,49 @@ export default function ShareSandboxModal({
{data.visibility === "private" ? ( {data.visibility === "private" ? (
<DialogDescription className="text-sm text-muted-foreground"> <DialogDescription className="text-sm text-muted-foreground">
This sandbox is private. Making it public will allow shared This sandbox is private. Making it public will allow shared
users to view and collaborate. users to view and collaborate. You can still share & manage access below.
</DialogDescription> </DialogDescription>
) : null} ) : null}
</DialogHeader> </DialogHeader>
<Form {...form}> <div className="flex space-x-4 w-full">
<form onSubmit={form.handleSubmit(onSubmit)} className="flex"> <Form {...form}>
<FormField <form onSubmit={form.handleSubmit(onSubmit)} className="flex w-full">
control={form.control} <FormField
name="email" control={form.control}
render={({ field }) => ( name="email"
<FormItem className="mr-4 w-full"> render={({ field }) => (
<FormControl> <FormItem className="mr-4 w-full">
<Input <FormControl>
placeholder="yourfriend@domain.com" <Input
{...field} placeholder="yourfriend@domain.com"
className="w-full" {...field}
/> className="w-full"
</FormControl> />
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<Button disabled={loading} type="submit" className=""> />
{loading ? ( <Button disabled={loading} type="submit" className="">
<> {loading ? (
<Loader2 className="animate-spin mr-2 h-4 w-4" /> Loading... <>
</> <Loader2 className="animate-spin mr-2 h-4 w-4" /> Loading...
) : ( </>
<> ) : (
<UserPlus className="mr-2 h-4 w-4" /> Share <>
</> <UserPlus className="mr-2 h-4 w-4" /> Share
)} </>
</Button> )}
</form> </Button>
</Form> </form>
</Form>
<Button onClick={() => {
navigator.clipboard.writeText(`https://s.ishaand.com/code/${data.id}`)
toast.success("Link copied to clipboard.")
}} size="icon" disabled={loading} variant="outline" className="shrink-0">
<Link className="h-4 w-4" />
</Button>
</div>
</div> </div>
{shared.length > 0 ? ( {shared.length > 0 ? (
<> <>

View File

@ -1,19 +1,26 @@
"use client" "use client";
import { Terminal } from "@xterm/xterm" import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit" import { FitAddon } from "@xterm/addon-fit";
import "./xterm.css" import "./xterm.css";
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react";
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client";
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react";
export default function EditorTerminal({ socket }: { socket: Socket }) { export default function EditorTerminal({
const terminalRef = useRef(null) socket,
const [term, setTerm] = useState<Terminal | null>(null) term,
setTerm,
}: {
socket: Socket;
term: Terminal | null;
setTerm: (term: Terminal) => void;
}) {
const terminalRef = useRef(null);
useEffect(() => { useEffect(() => {
if (!terminalRef.current) return if (!terminalRef.current) return;
const terminal = new Terminal({ const terminal = new Terminal({
cursorBlink: true, cursorBlink: true,
@ -22,55 +29,43 @@ export default function EditorTerminal({ socket }: { socket: Socket }) {
}, },
fontFamily: "var(--font-geist-mono)", fontFamily: "var(--font-geist-mono)",
fontSize: 14, fontSize: 14,
}) });
setTerm(terminal) setTerm(terminal);
return () => { return () => {
if (terminal) terminal.dispose() if (terminal) terminal.dispose();
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
if (!term) return if (!term) return;
const onConnect = () => { // const onTerminalResponse = (response: { data: string }) => {
console.log("Connected to server", socket.connected) // const res = response.data;
setTimeout(() => { // term.write(res);
socket.emit("createTerminal", { id: "testId" }) // };
}, 2000)
}
const onTerminalResponse = (response: { data: string }) => {
// const res = Buffer.from(response.data, "base64").toString("utf-8")
const res = response.data
term.write(res)
}
socket.on("connect", onConnect)
if (terminalRef.current) { if (terminalRef.current) {
socket.on("terminalResponse", onTerminalResponse) // socket.on("terminalResponse", onTerminalResponse);
const fitAddon = new FitAddon() const fitAddon = new FitAddon();
term.loadAddon(fitAddon) term.loadAddon(fitAddon);
term.open(terminalRef.current) term.open(terminalRef.current);
fitAddon.fit() fitAddon.fit();
setTerm(term) setTerm(term);
} }
const disposable = term.onData((data) => { const disposable = term.onData((data) => {
console.log("sending data", data) console.log("sending data", data);
socket.emit("terminalData", "testId", data) socket.emit("terminalData", "testId", data);
}) });
socket.emit("terminalData", "\n") // socket.emit("terminalData", "\n");
return () => { return () => {
socket.off("connect", onConnect) disposable.dispose();
socket.off("terminalResponse", onTerminalResponse) };
disposable.dispose() }, [term, terminalRef.current]);
}
}, [term, terminalRef.current])
return ( return (
<div ref={terminalRef} className="w-full h-full text-left"> <div ref={terminalRef} className="w-full h-full text-left">
@ -81,5 +76,5 @@ export default function EditorTerminal({ socket }: { socket: Socket }) {
</div> </div>
) : null} ) : null}
</div> </div>
) );
} }