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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import Image from "next/image"
import Logo from "@/assets/logo.svg"
import { Skeleton } from "../ui/skeleton"
import { Loader, Loader2 } from "lucide-react"
import Image from "next/image";
import Logo from "@/assets/logo.svg";
import { Skeleton } from "../ui/skeleton";
import { Loader, Loader2 } from "lucide-react";
export default function Loading() {
return (
@ -31,11 +31,15 @@ export default function Loading() {
</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">
<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">
<Loader2 className="w-6 h-6 mr-3 animate-spin" />
Loading...
</div>{" "}
</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>
);
}

View File

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

View File

@ -18,7 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form"
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 { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button"
@ -75,12 +75,13 @@ export default function ShareSandboxModal({
{data.visibility === "private" ? (
<DialogDescription className="text-sm text-muted-foreground">
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>
) : null}
</DialogHeader>
<div className="flex space-x-4 w-full">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex">
<form onSubmit={form.handleSubmit(onSubmit)} className="flex w-full">
<FormField
control={form.control}
name="email"
@ -110,6 +111,13 @@ export default function ShareSandboxModal({
</Button>
</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>
{shared.length > 0 ? (
<>

View File

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