improve starting server logic
This commit is contained in:
parent
07d59cf01b
commit
010a4fec59
@ -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=
|
||||
|
||||
|
@ -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 (
|
||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||
<Room id={sandboxId}>
|
||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
||||
<div className="w-screen flex grow">
|
||||
<Editor
|
||||
isOwner={isOwner}
|
||||
userData={userData}
|
||||
sandboxData={sandboxData}
|
||||
/>
|
||||
<Editor userData={userData} sandboxData={sandboxData} />
|
||||
</div>
|
||||
</Room>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -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<string>();
|
||||
const router = useRouter();
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [date, setDate] = useState<string>()
|
||||
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 (
|
||||
<Card
|
||||
@ -52,7 +52,7 @@ export default function ProjectCard({
|
||||
onMouseLeave={() => 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`}
|
||||
>
|
||||
{/* <AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@ -62,7 +62,7 @@ export default function ProjectCard({
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence> */}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||
<Image
|
||||
@ -101,5 +101,5 @@ export default function ProjectCard({
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -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<TTab[]>([]);
|
||||
const [activeFileId, setActiveFileId] = useState<string>("");
|
||||
const [activeFileContent, setActiveFileContent] = useState("");
|
||||
const [deletingFolderId, setDeletingFolderId] = useState("");
|
||||
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
|
||||
const [tabs, setTabs] = useState<TTab[]>([])
|
||||
const [activeFileId, setActiveFileId] = useState<string>("")
|
||||
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<monaco.editor.IStandaloneCodeEditor>();
|
||||
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||
|
||||
// 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<TypedLiveblocksProvider>();
|
||||
const room = useRoom()
|
||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||
|
||||
// Refs for libraries / features
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||
const monacoRef = useRef<typeof monaco | null>(null);
|
||||
const generateRef = useRef<HTMLDivElement>(null);
|
||||
const generateWidgetRef = useRef<HTMLDivElement>(null);
|
||||
const previewPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const editorPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||
const monacoRef = useRef<typeof monaco | null>(null)
|
||||
const generateRef = useRef<HTMLDivElement>(null)
|
||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const editorPanelRef = useRef<ImperativePanelHandle>(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({
|
||||
/>
|
||||
<Loading />
|
||||
</>
|
||||
);
|
||||
)
|
||||
|
||||
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({
|
||||
<PreviewWindow
|
||||
collapsed={isPreviewCollapsed}
|
||||
open={() => {
|
||||
previewPanelRef.current?.expand();
|
||||
setIsPreviewCollapsed(false);
|
||||
previewPanelRef.current?.expand()
|
||||
setIsPreviewCollapsed(false)
|
||||
}}
|
||||
ip={ip}
|
||||
/>
|
||||
@ -761,5 +760,5 @@ export default function CodeEditor({
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -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<HTMLInputElement>(null);
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="w-full pr-4 space-y-2">
|
||||
@ -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}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -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: () => <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<string>();
|
||||
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<string>()
|
||||
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);
|
||||
|
||||
checkServiceStatus(sandboxData.id)
|
||||
.then(() => {
|
||||
setIsDeploymentActive(true);
|
||||
|
||||
getTaskIp(sandboxData.id)
|
||||
.then((ip) => {
|
||||
setTaskIp(ip);
|
||||
setupServer({
|
||||
sandboxId: sandboxData.id,
|
||||
setIsServiceRunning,
|
||||
setIsDeploymentActive,
|
||||
setTaskIp,
|
||||
setDidFail,
|
||||
toast,
|
||||
})
|
||||
.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 <Loading didFail={didFail} />;
|
||||
if (didFail) return <Loading didFail={didFail} />
|
||||
if (!isServiceRunning || !isDeploymentActive || !taskIp)
|
||||
return (
|
||||
<Loading
|
||||
@ -75,9 +58,9 @@ export default function Editor({
|
||||
: "Requesting your server creation..."
|
||||
}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<CodeEditor ip={taskIp} userData={userData} sandboxData={sandboxData} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -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.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Service> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
export async function setupServer({
|
||||
sandboxId,
|
||||
setIsServiceRunning,
|
||||
setIsDeploymentActive,
|
||||
setTaskIp,
|
||||
setDidFail,
|
||||
toast,
|
||||
}: {
|
||||
sandboxId: string
|
||||
setIsServiceRunning: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setIsDeploymentActive: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setTaskIp: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
setDidFail: React.Dispatch<React.SetStateAction<boolean>>
|
||||
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)
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user