diff --git a/frontend/.env.example b/frontend/.env.example
index 908ff5a..0f1864a 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -3,6 +3,8 @@ CLERK_SECRET_KEY=
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
LIVEBLOCKS_SECRET_KEY=
+VERCEL_ENV=development
+
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
diff --git a/frontend/app/(app)/code/[id]/page.tsx b/frontend/app/(app)/code/[id]/page.tsx
index 11365e4..aeb84cf 100644
--- a/frontend/app/(app)/code/[id]/page.tsx
+++ b/frontend/app/(app)/code/[id]/page.tsx
@@ -1,79 +1,75 @@
-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 { notFound, redirect } from "next/navigation";
-import Editor from "@/components/editor";
+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 { notFound, redirect } from "next/navigation"
+import Editor from "@/components/editor"
-export const revalidate = 0;
+export const revalidate = 0
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[]) => {
if (!usersToSandboxes) {
- return [];
+ return []
}
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);
+ const isOwner = sandboxData.userId === user.id
+ const isSharedUser = shared.some((uts) => uts.id === user.id)
if (!isOwner && !isSharedUser) {
- return notFound();
+ return notFound()
}
- console.log("sandboxes: ", sandboxData);
+ console.log("sandboxes: ", sandboxData)
return (
- );
+ )
}
diff --git a/frontend/components/dashboard/projectCard/index.tsx b/frontend/components/dashboard/projectCard/index.tsx
index a206499..b082140 100644
--- a/frontend/components/dashboard/projectCard/index.tsx
+++ b/frontend/components/dashboard/projectCard/index.tsx
@@ -1,13 +1,13 @@
-"use client";
+"use client"
-import { AnimatePresence, motion } from "framer-motion";
-import Image from "next/image";
-import { useEffect, useState } from "react";
-import ProjectCardDropdown from "./dropdown";
-import { Clock, Globe, Lock } from "lucide-react";
-import { Sandbox } from "@/lib/types";
-import { Card } from "@/components/ui/card";
-import { useRouter } from "next/navigation";
+import { AnimatePresence, motion } from "framer-motion"
+import Image from "next/image"
+import { useEffect, useState } from "react"
+import ProjectCardDropdown from "./dropdown"
+import { Clock, Globe, Lock } from "lucide-react"
+import { Sandbox } from "@/lib/types"
+import { Card } from "@/components/ui/card"
+import { useRouter } from "next/navigation"
export default function ProjectCard({
children,
@@ -16,33 +16,33 @@ export default function ProjectCard({
onDelete,
deletingId,
}: {
- children?: React.ReactNode;
- sandbox: Sandbox;
- onVisibilityChange: (sandbox: Sandbox) => void;
- onDelete: (sandbox: Sandbox) => void;
- deletingId: string;
+ children?: React.ReactNode
+ sandbox: Sandbox
+ onVisibilityChange: (sandbox: Sandbox) => void
+ onDelete: (sandbox: Sandbox) => void
+ deletingId: string
}) {
- const [hovered, setHovered] = useState(false);
- const [date, setDate] = useState();
- const router = useRouter();
+ const [hovered, setHovered] = useState(false)
+ const [date, setDate] = useState()
+ const router = useRouter()
useEffect(() => {
- const createdAt = new Date(sandbox.createdAt);
- const now = new Date();
+ const createdAt = new Date(sandbox.createdAt)
+ const now = new Date()
const diffInMinutes = Math.floor(
(now.getTime() - createdAt.getTime()) / 60000
- );
+ )
if (diffInMinutes < 1) {
- setDate("Now");
+ setDate("Now")
} else if (diffInMinutes < 60) {
- setDate(`${diffInMinutes}m ago`);
+ setDate(`${diffInMinutes}m ago`)
} else if (diffInMinutes < 1440) {
- setDate(`${Math.floor(diffInMinutes / 60)}h ago`);
+ setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
} else {
- setDate(`${Math.floor(diffInMinutes / 1440)}d ago`);
+ setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
}
- }, [sandbox]);
+ }, [sandbox])
return (
setHovered(false)}
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
>
- {/*
+
{hovered && (
)}
- */}
+
- );
+ )
}
diff --git a/frontend/components/editor/editor.tsx b/frontend/components/editor/editor.tsx
index 1cfc8c9..c3f3a20 100644
--- a/frontend/components/editor/editor.tsx
+++ b/frontend/components/editor/editor.tsx
@@ -1,110 +1,109 @@
-"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";
-import { FileJson, Loader2, TerminalSquare } from "lucide-react";
-import Tab from "../ui/tab";
-import Sidebar from "./sidebar";
-import GenerateInput from "./generate";
-import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types";
-import { addNew, processFileType, validateName } from "@/lib/utils";
-import { Cursors } from "./live/cursors";
-import { Terminal } from "@xterm/xterm";
-import DisableAccessModal from "./live/disableModal";
-import Loading from "./loading";
-import PreviewWindow from "./preview";
-import Terminals from "./terminals";
-import { ImperativePanelHandle } from "react-resizable-panels";
+} from "@/components/ui/resizable"
+import { FileJson, Loader2, TerminalSquare } from "lucide-react"
+import Tab from "../ui/tab"
+import Sidebar from "./sidebar"
+import GenerateInput from "./generate"
+import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
+import { addNew, processFileType, validateName } from "@/lib/utils"
+import { Cursors } from "./live/cursors"
+import { Terminal } from "@xterm/xterm"
+import DisableAccessModal from "./live/disableModal"
+import Loading from "./loading"
+import PreviewWindow from "./preview"
+import Terminals from "./terminals"
+import { ImperativePanelHandle } from "react-resizable-panels"
export default function CodeEditor({
userData,
sandboxData,
ip,
}: {
- userData: User;
- sandboxData: Sandbox;
- ip: string;
+ userData: User
+ sandboxData: Sandbox
+ ip: string
}) {
const socket = io(
- // `ws://${sandboxData.id}.ws.ishaand.com?userId=${userData.id}&sandboxId=${sandboxData.id}`
`http://${ip}:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{
timeout: 2000,
}
- );
+ )
- const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
+ const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [disableAccess, setDisableAccess] = useState({
isDisabled: false,
message: "",
- });
+ })
// File state
- const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
- const [tabs, setTabs] = useState([]);
- const [activeFileId, setActiveFileId] = useState("");
- const [activeFileContent, setActiveFileContent] = useState("");
- const [deletingFolderId, setDeletingFolderId] = useState("");
+ const [files, setFiles] = useState<(TFolder | TFile)[]>([])
+ const [tabs, setTabs] = useState([])
+ const [activeFileId, setActiveFileId] = useState("")
+ const [activeFileContent, setActiveFileContent] = useState("")
+ const [deletingFolderId, setDeletingFolderId] = useState("")
// Editor state
- const [editorLanguage, setEditorLanguage] = useState("plaintext");
- const [cursorLine, setCursorLine] = useState(0);
+ const [editorLanguage, setEditorLanguage] = useState("plaintext")
+ const [cursorLine, setCursorLine] = useState(0)
const [editorRef, setEditorRef] =
- useState();
+ useState()
// AI Copilot state
- const [ai, setAi] = useState(false);
+ const [ai, setAi] = useState(false)
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 });
+ options: monaco.editor.IModelDeltaDecoration[]
+ instance: monaco.editor.IEditorDecorationsCollection | undefined
+ }>({ options: [], instance: undefined })
// Terminal state
const [terminals, setTerminals] = useState<
{
- id: string;
- terminal: Terminal | null;
+ id: string
+ terminal: Terminal | null
}[]
- >([]);
+ >([])
- const isOwner = sandboxData.userId === userData.id;
- const clerk = useClerk();
+ const isOwner = sandboxData.userId === userData.id
+ const clerk = useClerk()
// Liveblocks hooks
- const room = useRoom();
- const [provider, setProvider] = useState();
+ const room = useRoom()
+ const [provider, setProvider] = useState()
// Refs for libraries / features
- const editorContainerRef = useRef(null);
- const monacoRef = useRef(null);
- const generateRef = useRef(null);
- const generateWidgetRef = useRef(null);
- const previewPanelRef = useRef(null);
- const editorPanelRef = useRef(null);
+ const editorContainerRef = useRef(null)
+ const monacoRef = useRef(null)
+ const generateRef = useRef(null)
+ const generateWidgetRef = useRef(null)
+ const previewPanelRef = useRef(null)
+ const editorPanelRef = useRef(null)
// Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => {
@@ -113,21 +112,21 @@ export default function CodeEditor({
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
command: "null",
},
- ]);
- };
+ ])
+ }
// Post-mount editor keybindings and actions
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 {
@@ -145,18 +144,18 @@ export default function CodeEditor({
},
},
],
- };
- });
- });
+ }
+ })
+ })
editor.onDidBlurEditorText((e) => {
setDecorations((prev) => {
return {
...prev,
options: [],
- };
- });
- });
+ }
+ })
+ })
editor.addAction({
id: "generate",
@@ -170,11 +169,11 @@ export default function CodeEditor({
...prev,
show: !prev.show,
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
- };
- });
+ }
+ })
},
- });
- };
+ })
+ }
// Generate widget effect
useEffect(() => {
@@ -183,32 +182,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 {
@@ -217,232 +216,232 @@ export default function CodeEditor({
column: 1,
},
preference: generate.pref,
- };
+ }
},
- };
+ }
// window width - sidebar width, times the percentage of the editor panel
const width = editorPanelRef.current
? (editorPanelRef.current.getSize() / 100) * (window.innerWidth - 224)
- : 400; //fallback
+ : 400 //fallback
setGenerate((prev) => {
return {
...prev,
widget: contentWidget,
width,
- };
- });
- editorRef?.addContentWidget(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])
// Decorations effect for generate widget tips
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])
// Save file keybinding logic effect
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
+ e.preventDefault()
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab
)
- );
+ )
- socket.emit("saveFile", activeFileId, editorRef?.getValue());
+ socket.emit("saveFile", activeFileId, editorRef?.getValue())
}
- };
- document.addEventListener("keydown", down);
+ }
+ document.addEventListener("keydown", down)
return () => {
- document.removeEventListener("keydown", down);
- };
- }, [tabs, activeFileId]);
+ document.removeEventListener("keydown", down)
+ }
+ }, [tabs, activeFileId])
// Liveblocks live collaboration setup effect
useEffect(() => {
- const tab = tabs.find((t) => t.id === activeFileId);
- 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 (activeFileContent) {
- yText.insert(0, activeFileContent);
+ yText.insert(0, activeFileContent)
} else {
setTimeout(() => {
- yText.insert(0, editorRef.getValue());
- }, 0);
+ yText.insert(0, editorRef.getValue())
+ }, 0)
}
}
}
- };
+ }
- 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, activeFileContent]);
+ yDoc.destroy()
+ yProvider.destroy()
+ binding.destroy()
+ yProvider.off("sync", onSync)
+ }
+ }, [editorRef, room, activeFileContent])
// Connection/disconnection effect
useEffect(() => {
- socket.connect();
+ socket.connect()
return () => {
- socket.disconnect();
- };
- }, []);
+ socket.disconnect()
+ }
+ }, [])
// Socket event listener effect
useEffect(() => {
- const onConnect = () => {};
+ const onConnect = () => {}
const onDisconnect = () => {
- setTerminals([]);
- };
+ setTerminals([])
+ }
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
- setFiles(files);
- };
+ setFiles(files)
+ }
const onRateLimit = (message: string) => {
- toast.error(message);
- };
+ toast.error(message)
+ }
const onTerminalResponse = (response: { id: string; data: string }) => {
- const term = terminals.find((t) => t.id === response.id);
+ const term = terminals.find((t) => t.id === response.id)
if (term && term.terminal) {
- term.terminal.write(response.data);
+ term.terminal.write(response.data)
}
- };
+ }
const onDisableAccess = (message: string) => {
if (!isOwner)
setDisableAccess({
isDisabled: true,
message,
- });
- };
+ })
+ }
- socket.on("connect", onConnect);
- socket.on("disconnect", onDisconnect);
- socket.on("loaded", onLoadedEvent);
- socket.on("rateLimit", onRateLimit);
- socket.on("terminalResponse", onTerminalResponse);
- socket.on("disableAccess", onDisableAccess);
+ socket.on("connect", onConnect)
+ socket.on("disconnect", onDisconnect)
+ socket.on("loaded", onLoadedEvent)
+ socket.on("rateLimit", onRateLimit)
+ socket.on("terminalResponse", onTerminalResponse)
+ socket.on("disableAccess", onDisableAccess)
return () => {
- socket.off("connect", onConnect);
- socket.off("disconnect", onDisconnect);
- socket.off("loaded", onLoadedEvent);
- socket.off("rateLimit", onRateLimit);
- socket.off("terminalResponse", onTerminalResponse);
- socket.off("disableAccess", onDisableAccess);
- };
+ socket.off("connect", onConnect)
+ socket.off("disconnect", onDisconnect)
+ socket.off("loaded", onLoadedEvent)
+ socket.off("rateLimit", onRateLimit)
+ socket.off("terminalResponse", onTerminalResponse)
+ socket.off("disableAccess", onDisableAccess)
+ }
// }, []);
- }, [terminals]);
+ }, [terminals])
// Helper functions for tabs:
// Select file and load content
const selectFile = (tab: TTab) => {
- if (tab.id === activeFileId) return;
+ if (tab.id === activeFileId) return
setGenerate((prev) => {
return {
...prev,
show: false,
- };
- });
- const exists = tabs.find((t) => t.id === tab.id);
+ }
+ })
+ const exists = tabs.find((t) => t.id === tab.id)
setTabs((prev) => {
if (exists) {
- setActiveFileId(exists.id);
- return prev;
+ setActiveFileId(exists.id)
+ return prev
}
- return [...prev, tab];
- });
+ return [...prev, tab]
+ })
socket.emit("getFile", tab.id, (response: string) => {
- setActiveFileContent(response);
- });
- setEditorLanguage(processFileType(tab.name));
- setActiveFileId(tab.id);
- };
+ setActiveFileContent(response)
+ })
+ setEditorLanguage(processFileType(tab.name))
+ setActiveFileId(tab.id)
+ }
// Close tab and remove from tabs
const closeTab = (id: string) => {
- const numTabs = tabs.length;
- const index = tabs.findIndex((t) => t.id === id);
+ const numTabs = tabs.length
+ const index = tabs.findIndex((t) => t.id === id)
- console.log("closing tab", id, index);
+ console.log("closing tab", id, index)
- if (index === -1) return;
+ if (index === -1) return
const nextId =
activeFileId === id
@@ -451,49 +450,49 @@ export default function CodeEditor({
: index < numTabs - 1
? tabs[index + 1].id
: tabs[index - 1].id
- : activeFileId;
+ : activeFileId
- setTabs((prev) => prev.filter((t) => t.id !== id));
+ setTabs((prev) => prev.filter((t) => t.id !== id))
if (!nextId) {
- setActiveFileId("");
+ 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 closeTabs = (ids: string[]) => {
- const numTabs = tabs.length;
+ const numTabs = tabs.length
- if (numTabs === 0) return;
+ if (numTabs === 0) return
- const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id));
+ const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id))
- const indexes = allIndexes.filter((index) => index !== -1);
- if (indexes.length === 0) return;
+ const indexes = allIndexes.filter((index) => index !== -1)
+ if (indexes.length === 0) return
- console.log("closing tabs", ids, indexes);
+ console.log("closing tabs", ids, indexes)
- const activeIndex = tabs.findIndex((t) => t.id === activeFileId);
+ const activeIndex = tabs.findIndex((t) => t.id === activeFileId)
- const newTabs = tabs.filter((t) => !ids.includes(t.id));
- setTabs(newTabs);
+ const newTabs = tabs.filter((t) => !ids.includes(t.id))
+ setTabs(newTabs)
if (indexes.length === numTabs) {
- setActiveFileId("");
+ setActiveFileId("")
} else {
const nextTab =
newTabs.length > activeIndex
? newTabs[activeIndex]
- : newTabs[newTabs.length - 1];
+ : newTabs[newTabs.length - 1]
if (nextTab) {
- selectFile(nextTab);
+ selectFile(nextTab)
}
}
- };
+ }
const handleRename = (
id: string,
@@ -501,40 +500,40 @@ 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.id);
- };
+ setFiles(response)
+ })
+ closeTab(file.id)
+ }
const handleDeleteFolder = (folder: TFolder) => {
- setDeletingFolderId(folder.id);
- console.log("deleting folder", folder.id);
+ setDeletingFolderId(folder.id)
+ console.log("deleting folder", folder.id)
socket.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response)
- );
+ )
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
- setFiles(response);
- setDeletingFolderId("");
- });
- };
+ setFiles(response)
+ setDeletingFolderId("")
+ })
+ }
// On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled)
@@ -547,7 +546,7 @@ export default function CodeEditor({
/>
>
- );
+ )
return (
<>
@@ -569,41 +568,41 @@ 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)
}}
onClose={() => {
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
- };
- });
+ }
+ })
}}
/>
) : null}
@@ -643,7 +642,7 @@ export default function CodeEditor({
saved={tab.saved}
selected={activeFileId === tab.id}
onClick={(e) => {
- selectFile(tab);
+ selectFile(tab)
}}
onClose={() => closeTab(tab.id)}
>
@@ -680,7 +679,7 @@ export default function CodeEditor({
? { ...tab, saved: true }
: tab
)
- );
+ )
} else {
setTabs((prev) =>
prev.map((tab) =>
@@ -688,7 +687,7 @@ export default function CodeEditor({
? { ...tab, saved: false }
: tab
)
- );
+ )
}
}}
options={{
@@ -732,8 +731,8 @@ export default function CodeEditor({
{
- previewPanelRef.current?.expand();
- setIsPreviewCollapsed(false);
+ previewPanelRef.current?.expand()
+ setIsPreviewCollapsed(false)
}}
ip={ip}
/>
@@ -761,5 +760,5 @@ export default function CodeEditor({
>
- );
+ )
}
diff --git a/frontend/components/editor/generate.tsx b/frontend/components/editor/generate.tsx
index e8bcc47..b1c6123 100644
--- a/frontend/components/editor/generate.tsx
+++ b/frontend/components/editor/generate.tsx
@@ -1,13 +1,13 @@
-"use client";
+"use client"
-import { useEffect, useRef, useState } from "react";
-import { Button } from "../ui/button";
-import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react";
-import { Socket } from "socket.io-client";
-import { Editor } from "@monaco-editor/react";
-import { User } from "@/lib/types";
-import { toast } from "sonner";
-import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useRef, useState } from "react"
+import { Button } from "../ui/button"
+import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
+import { Socket } from "socket.io-client"
+import { Editor } from "@monaco-editor/react"
+import { User } from "@/lib/types"
+import { toast } from "sonner"
+import { usePathname, useRouter } from "next/navigation"
// import monaco from "monaco-editor"
export default function GenerateInput({
@@ -20,54 +20,54 @@ export default function GenerateInput({
onAccept,
onClose,
}: {
- user: User;
- socket: Socket;
- width: number;
+ user: User
+ socket: Socket
+ width: number
data: {
- fileName: string;
- code: string;
- line: number;
- };
+ fileName: string
+ code: string
+ line: number
+ }
editor: {
- language: string;
- };
- onExpand: () => void;
- onAccept: (code: string) => void;
- onClose: () => void;
+ language: string
+ }
+ onExpand: () => void
+ onAccept: (code: string) => void
+ onClose: () => void
}) {
- const pathname = usePathname();
- const router = useRouter();
- const inputRef = useRef(null);
+ const pathname = usePathname()
+ const router = useRouter()
+ const inputRef = useRef(null)
- const [code, setCode] = useState("");
- const [expanded, setExpanded] = useState(false);
+ const [code, setCode] = useState("")
+ const [expanded, setExpanded] = useState(false)
const [loading, setLoading] = useState({
generate: false,
regenerate: false,
- });
- const [input, setInput] = useState("");
- const [currentPrompt, setCurrentPrompt] = useState("");
+ })
+ const [input, setInput] = useState("")
+ const [currentPrompt, setCurrentPrompt] = useState("")
useEffect(() => {
setTimeout(() => {
- inputRef.current?.focus();
- }, 100);
- }, [inputRef.current]);
+ inputRef.current?.focus()
+ }, 100)
+ }, [inputRef.current])
const handleGenerate = async ({
regenerate = false,
}: {
- regenerate?: boolean;
+ regenerate?: boolean
}) => {
if (user.generations >= 10) {
toast.error(
"You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)"
- );
- return;
+ )
+ return
}
- setLoading({ generate: !regenerate, regenerate });
- setCurrentPrompt(input);
+ setLoading({ generate: !regenerate, regenerate })
+ setCurrentPrompt(input)
socket.emit(
"generateCode",
data.fileName,
@@ -75,25 +75,25 @@ export default function GenerateInput({
data.line,
regenerate ? currentPrompt : input,
(res: { response: string; success: boolean }) => {
- console.log("Generated code", res.response, res.success);
+ console.log("Generated code", res.response, res.success)
// if (!res.success) {
// toast.error("Failed to generate code.");
// return;
// }
- setCode(res.response);
- router.refresh();
+ setCode(res.response)
+ router.refresh()
}
- );
- };
+ )
+ }
useEffect(() => {
if (code) {
- setExpanded(true);
- onExpand();
- setLoading({ generate: false, regenerate: false });
+ setExpanded(true)
+ onExpand()
+ setLoading({ generate: false, regenerate: false })
}
- }, [code]);
+ }, [code])
return (
@@ -105,7 +105,7 @@ export default function GenerateInput({
}}
value={input}
onChange={(e) => setInput(e.target.value)}
- placeholder="✨ Generate code with a prompt"
+ placeholder="Generate code with a prompt"
className="h-8 w-full rounded-md border border-muted-foreground bg-transparent px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
@@ -192,5 +192,5 @@ export default function GenerateInput({
>
) : null}
- );
+ )
}
diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx
index 25b37fd..ee556a1 100644
--- a/frontend/components/editor/index.tsx
+++ b/frontend/components/editor/index.tsx
@@ -1,68 +1,51 @@
-"use client";
+"use client"
-import dynamic from "next/dynamic";
-import Loading from "@/components/editor/loading";
-import { Sandbox, User } from "@/lib/types";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
-import { getTaskIp, startServer } from "@/lib/actions";
-import { checkServiceStatus } from "@/lib/utils";
+import dynamic from "next/dynamic"
+import Loading from "@/components/editor/loading"
+import { Sandbox, User } from "@/lib/types"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import { getTaskIp, startServer } from "@/lib/actions"
+import { checkServiceStatus, setupServer } from "@/lib/utils"
const CodeEditor = dynamic(() => import("@/components/editor/editor"), {
ssr: false,
loading: () => ,
-});
+})
export default function Editor({
- isOwner,
userData,
sandboxData,
}: {
- isOwner: boolean;
- userData: User;
- sandboxData: Sandbox;
+ userData: User
+ sandboxData: Sandbox
}) {
- const [isServiceRunning, setIsServiceRunning] = useState(false);
- const [isDeploymentActive, setIsDeploymentActive] = useState(false);
- const [taskIp, setTaskIp] = useState();
- const [didFail, setDidFail] = useState(false);
+ const isDev = process.env.VERCEL_ENV === "development"
+
+ const [isServiceRunning, setIsServiceRunning] = useState(false)
+ const [isDeploymentActive, setIsDeploymentActive] = useState(false)
+ const [taskIp, setTaskIp] = useState()
+ const [didFail, setDidFail] = useState(false)
useEffect(() => {
- if (!isOwner) {
- toast.error("You are not the owner of this sandbox. (TEMPORARY)");
- setDidFail(true);
- return;
+ if (isDev) {
+ setIsServiceRunning(true)
+ setIsDeploymentActive(true)
+ setTaskIp("localhost")
+ return
}
- startServer(sandboxData.id).then((response) => {
- if (!response.success) {
- toast.error(response.message);
- setDidFail(true);
- } else {
- setIsServiceRunning(true);
+ setupServer({
+ sandboxId: sandboxData.id,
+ setIsServiceRunning,
+ setIsDeploymentActive,
+ setTaskIp,
+ setDidFail,
+ toast,
+ })
+ }, [])
- checkServiceStatus(sandboxData.id)
- .then(() => {
- setIsDeploymentActive(true);
-
- getTaskIp(sandboxData.id)
- .then((ip) => {
- setTaskIp(ip);
- })
- .catch(() => {
- setDidFail(true);
- toast.error("An error occurred while getting your server IP.");
- });
- })
- .catch(() => {
- toast.error("An error occurred while initializing your server.");
- setDidFail(true);
- });
- }
- });
- }, []);
-
- if (didFail) return ;
+ if (didFail) return
if (!isServiceRunning || !isDeploymentActive || !taskIp)
return (
- );
+ )
return (
- );
+ )
}
diff --git a/frontend/lib/actions.ts b/frontend/lib/actions.ts
index 432b859..d335a87 100644
--- a/frontend/lib/actions.ts
+++ b/frontend/lib/actions.ts
@@ -1,21 +1,21 @@
-"use server";
+"use server"
-import { revalidatePath } from "next/cache";
-import ecsClient, { ec2Client } from "./ecs";
+import { revalidatePath } from "next/cache"
+import ecsClient, { ec2Client } from "./ecs"
import {
CreateServiceCommand,
DescribeClustersCommand,
DescribeServicesCommand,
DescribeTasksCommand,
ListTasksCommand,
-} from "@aws-sdk/client-ecs";
-import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2";
+} from "@aws-sdk/client-ecs"
+import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2"
export async function createSandbox(body: {
- type: string;
- name: string;
- userId: string;
- visibility: string;
+ type: string
+ name: string
+ userId: string
+ visibility: string
}) {
const res = await fetch(
"https://database.ishaan1013.workers.dev/api/sandbox",
@@ -26,15 +26,15 @@ export async function createSandbox(body: {
},
body: JSON.stringify(body),
}
- );
+ )
- return await res.text();
+ return await res.text()
}
export async function updateSandbox(body: {
- id: string;
- name?: string;
- visibility?: "public" | "private";
+ id: string
+ name?: string
+ visibility?: "public" | "private"
}) {
await fetch("https://database.ishaan1013.workers.dev/api/sandbox", {
method: "POST",
@@ -42,17 +42,17 @@ export async function updateSandbox(body: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
- });
+ })
- revalidatePath("/dashboard");
+ revalidatePath("/dashboard")
}
export async function deleteSandbox(id: string) {
await fetch(`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`, {
method: "DELETE",
- });
+ })
- revalidatePath("/dashboard");
+ revalidatePath("/dashboard")
}
export async function shareSandbox(sandboxId: string, email: string) {
@@ -65,15 +65,15 @@ export async function shareSandbox(sandboxId: string, email: string) {
},
body: JSON.stringify({ sandboxId, email }),
}
- );
- const text = await res.text();
+ )
+ const text = await res.text()
if (res.status !== 200) {
- return { success: false, message: text };
+ return { success: false, message: text }
}
- revalidatePath(`/code/${sandboxId}`);
- return { success: true, message: "Shared successfully." };
+ revalidatePath(`/code/${sandboxId}`)
+ return { success: true, message: "Shared successfully." }
}
export async function unshareSandbox(sandboxId: string, userId: string) {
@@ -83,102 +83,102 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
"Content-Type": "application/json",
},
body: JSON.stringify({ sandboxId, userId }),
- });
+ })
- revalidatePath(`/code/${sandboxId}`);
+ revalidatePath(`/code/${sandboxId}`)
}
export async function describeService(serviceName: string) {
const command = new DescribeServicesCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
services: [serviceName],
- });
+ })
- return await ecsClient.send(command);
+ return await ecsClient.send(command)
}
export async function getTaskIp(serviceName: string) {
const listCommand = new ListTasksCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
serviceName,
- });
+ })
- const listResponse = await ecsClient.send(listCommand);
- const taskArns = listResponse.taskArns;
+ const listResponse = await ecsClient.send(listCommand)
+ const taskArns = listResponse.taskArns
const describeCommand = new DescribeTasksCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
tasks: taskArns,
- });
+ })
- const describeResponse = await ecsClient.send(describeCommand);
- const tasks = describeResponse.tasks;
- const taskAttachment = tasks?.[0].attachments?.[0].details;
+ const describeResponse = await ecsClient.send(describeCommand)
+ const tasks = describeResponse.tasks
+ const taskAttachment = tasks?.[0].attachments?.[0].details
if (!taskAttachment) {
- throw new Error("Task attachment not found");
+ throw new Error("Task attachment not found")
}
const eni = taskAttachment.find(
(detail) => detail.name === "networkInterfaceId"
- )?.value;
+ )?.value
if (!eni) {
- throw new Error("Network interface not found");
+ throw new Error("Network interface not found")
}
const describeNetworkInterfacesCommand = new DescribeNetworkInterfacesCommand(
{
NetworkInterfaceIds: [eni],
}
- );
+ )
const describeNetworkInterfacesResponse = await ec2Client.send(
describeNetworkInterfacesCommand
- );
+ )
const ip =
describeNetworkInterfacesResponse.NetworkInterfaces?.[0].Association
- ?.PublicIp;
+ ?.PublicIp
if (!ip) {
- throw new Error("Public IP not found");
+ throw new Error("Public IP not found")
}
- return ip;
+ return ip
}
-async function doesServiceExist(serviceName: string) {
- const response = await describeService(serviceName);
+export async function doesServiceExist(serviceName: string) {
+ const response = await describeService(serviceName)
const activeServices = response.services?.filter(
(service) => service.status === "ACTIVE"
- );
+ )
- console.log("activeServices: ", activeServices);
+ console.log("activeServices: ", activeServices)
- return activeServices?.length === 1;
+ return activeServices?.length === 1
}
async function countServices() {
const command = new DescribeClustersCommand({
clusters: [process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!],
- });
+ })
- const response = await ecsClient.send(command);
- return response.clusters?.[0].activeServicesCount!;
+ const response = await ecsClient.send(command)
+ return response.clusters?.[0].activeServicesCount!
}
export async function startServer(
serviceName: string
): Promise<{ success: boolean; message: string }> {
- const serviceExists = await doesServiceExist(serviceName);
+ const serviceExists = await doesServiceExist(serviceName)
if (serviceExists) {
- return { success: true, message: "" };
+ return { success: true, message: "" }
}
- const activeServices = await countServices();
+ const activeServices = await countServices()
if (activeServices >= 100) {
return {
success: false,
message:
"Too many servers are running! Please try again later or contact @ishaandey_ on Twitter/X.",
- };
+ }
}
const command = new CreateServiceCommand({
@@ -201,13 +201,13 @@ export async function startServer(
assignPublicIp: "ENABLED",
},
},
- });
+ })
try {
- const response = await ecsClient.send(command);
- console.log("started server:", response.service?.serviceName);
+ const response = await ecsClient.send(command)
+ console.log("started server:", response.service?.serviceName)
- return { success: true, message: "" };
+ return { success: true, message: "" }
// store in workers kv:
// {
@@ -223,6 +223,6 @@ export async function startServer(
return {
success: false,
message: `Error starting server: ${error.message}. Try again in a minute, or contact @ishaandey_ on Twitter/X if it still doesn't work.`,
- };
+ }
}
}
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts
index 137d45d..c56ff4b 100644
--- a/frontend/lib/utils.ts
+++ b/frontend/lib/utils.ts
@@ -1,22 +1,27 @@
-import { type ClassValue, clsx } from "clsx";
+import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner"
-import { twMerge } from "tailwind-merge";
-import { Sandbox, TFile, TFolder } from "./types";
-import { Service } from "@aws-sdk/client-ecs";
-import { describeService } from "./actions";
+import { twMerge } from "tailwind-merge"
+import { Sandbox, TFile, TFolder } from "./types"
+import { Service } from "@aws-sdk/client-ecs"
+import {
+ describeService,
+ doesServiceExist,
+ getTaskIp,
+ startServer,
+} from "./actions"
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
+ return twMerge(clsx(inputs))
}
export function processFileType(file: string) {
- const ending = file.split(".").pop();
+ const ending = file.split(".").pop()
- if (ending === "ts" || ending === "tsx") return "typescript";
- if (ending === "js" || ending === "jsx") return "javascript";
+ if (ending === "ts" || ending === "tsx") return "typescript"
+ if (ending === "js" || ending === "jsx") return "javascript"
- if (ending) return ending;
- return "plaintext";
+ if (ending) return ending
+ return "plaintext"
}
export function validateName(
@@ -25,7 +30,7 @@ export function validateName(
type: "file" | "folder"
) {
if (newName === oldName || newName.length === 0) {
- return { status: false, message: "" };
+ return { status: false, message: "" }
}
if (
newName.includes("/") ||
@@ -34,9 +39,9 @@ export function validateName(
(type === "file" && !newName.includes(".")) ||
(type === "folder" && newName.includes("."))
) {
- return { status: false, message: "Invalid file name." };
+ return { status: false, message: "Invalid file name." }
}
- return { status: true, message: "" };
+ return { status: true, message: "" }
}
export function addNew(
@@ -49,9 +54,9 @@ export function addNew(
setFiles((prev) => [
...prev,
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
- ]);
+ ])
} else {
- console.log("adding folder");
+ console.log("adding folder")
setFiles((prev) => [
...prev,
{
@@ -60,48 +65,92 @@ export function addNew(
type: "folder",
children: [],
},
- ]);
+ ])
}
}
export function checkServiceStatus(serviceName: string): Promise {
return new Promise((resolve, reject) => {
- let tries = 0;
+ let tries = 0
const interval = setInterval(async () => {
try {
- tries++;
+ tries++
if (tries > 40) {
- clearInterval(interval);
- reject(new Error("Timed out."));
+ clearInterval(interval)
+ reject(new Error("Timed out."))
}
- const response = await describeService(serviceName);
+ const response = await describeService(serviceName)
const activeServices = response.services?.filter(
(service) => service.status === "ACTIVE"
- );
- console.log("Checking activeServices status", activeServices);
+ )
+ console.log("Checking activeServices status", activeServices)
if (activeServices?.length === 1) {
- const service = activeServices?.[0];
+ const service = activeServices?.[0]
if (
service.runningCount === service.desiredCount &&
service.deployments?.length === 1
) {
if (service.deployments[0].rolloutState === "COMPLETED") {
- clearInterval(interval);
- resolve(service);
+ clearInterval(interval)
+ resolve(service)
} else if (service.deployments[0].rolloutState === "FAILED") {
- clearInterval(interval);
- reject(new Error("Deployment failed."));
+ clearInterval(interval)
+ reject(new Error("Deployment failed."))
}
}
}
} catch (error: any) {
- clearInterval(interval);
- reject(error);
+ clearInterval(interval)
+ reject(error)
}
- }, 3000);
- });
+ }, 3000)
+ })
+}
+
+export async function setupServer({
+ sandboxId,
+ setIsServiceRunning,
+ setIsDeploymentActive,
+ setTaskIp,
+ setDidFail,
+ toast,
+}: {
+ sandboxId: string
+ setIsServiceRunning: React.Dispatch>
+ setIsDeploymentActive: React.Dispatch>
+ setTaskIp: React.Dispatch>
+ setDidFail: React.Dispatch>
+ toast: any
+}) {
+ const doesExist = await doesServiceExist(sandboxId)
+
+ if (!doesExist) {
+ const response = await startServer(sandboxId)
+
+ if (!response.success) {
+ toast.error(response.message)
+ setDidFail(true)
+ return
+ }
+ }
+
+ setIsServiceRunning(true)
+
+ try {
+ if (!doesExist) {
+ await checkServiceStatus(sandboxId)
+ }
+
+ setIsDeploymentActive(true)
+
+ const taskIp = await getTaskIp(sandboxId)
+ setTaskIp(taskIp)
+ } catch (error) {
+ toast.error("An error occurred while initializing your server.")
+ setDidFail(true)
+ }
}