From deb32352fb15648d0be7225ea910a508273c9a81 Mon Sep 17 00:00:00 2001 From: Akhilesh Rangani Date: Tue, 23 Jul 2024 17:30:35 -0400 Subject: [PATCH] feat: add run button --- frontend/app/layout.tsx | 8 +- frontend/components/editor/index.tsx | 454 +++++++++--------- frontend/components/editor/navbar/index.tsx | 19 +- frontend/components/editor/navbar/run.tsx | 59 +++ frontend/components/editor/preview/index.tsx | 25 +- .../components/editor/terminals/index.tsx | 100 ++-- frontend/context/PreviewContext.tsx | 34 ++ frontend/context/TerminalContext.tsx | 117 +++++ 8 files changed, 518 insertions(+), 298 deletions(-) create mode 100644 frontend/components/editor/navbar/run.tsx create mode 100644 frontend/context/PreviewContext.tsx create mode 100644 frontend/context/TerminalContext.tsx diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 50ee950..79f8b5d 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -6,6 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider" import { ClerkProvider } from "@clerk/nextjs" import { Toaster } from "@/components/ui/sonner" import { Analytics } from "@vercel/analytics/react" +import { TerminalProvider } from '@/context/TerminalContext'; +import { PreviewProvider } from "@/context/PreviewContext" export const metadata: Metadata = { title: "Sandbox", @@ -27,7 +29,11 @@ export default function RootLayout({ forcedTheme="dark" disableTransitionOnChange > + + {children} + + @@ -35,4 +41,4 @@ export default function RootLayout({ ) -} +} \ No newline at end of file diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 495951e..a00fcd8 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -31,6 +31,8 @@ import Loading from "./loading" import PreviewWindow from "./preview" import Terminals from "./terminals" import { ImperativePanelHandle } from "react-resizable-panels" +import { PreviewProvider, usePreview } from '@/context/PreviewContext'; +import { useTerminal } from '@/context/TerminalContext'; export default function CodeEditor({ userData, @@ -50,8 +52,17 @@ export default function CodeEditor({ { timeout: 2000, } - );} + ); + } + //Terminalcontext functionsand effects + const { setUserAndSandboxId } = useTerminal(); + + useEffect(() => { + setUserAndSandboxId(userData.id, sandboxData.id); + }, [userData.id, sandboxData.id, setUserAndSandboxId]); + + //Preview Button state const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [disableAccess, setDisableAccess] = useState({ isDisabled: false, @@ -104,7 +115,7 @@ export default function CodeEditor({ const room = useRoom() const [provider, setProvider] = useState() const userInfo = useSelf((me) => me.info) - + // Liveblocks providers map to prevent reinitializing providers type ProviderData = { provider: LiveblocksProvider; @@ -317,7 +328,7 @@ export default function CodeEditor({ console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${value}`); socketRef.current?.emit("saveFile", activeFileId, value); - }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY)||1000), + }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), [socketRef] ); @@ -343,7 +354,7 @@ export default function CodeEditor({ if (!editorRef || !tab || !model) return let providerData: ProviderData; - + // When a file is opened for the first time, create a new provider and store in providersMap. if (!providersMap.current.has(tab.id)) { const yDoc = new Y.Doc(); @@ -385,7 +396,7 @@ export default function CodeEditor({ ); providerData.binding = binding; - + setProvider(providerData.provider); return () => { @@ -399,25 +410,25 @@ export default function CodeEditor({ }; }, [room, activeFileContent]); - // Added this effect to clean up when the component unmounts - useEffect(() => { - return () => { - // Clean up all providers when the component unmounts - providersMap.current.forEach((data) => { - if (data.binding) { - data.binding.destroy(); - } - data.provider.disconnect(); - data.yDoc.destroy(); - }); - providersMap.current.clear(); - }; - }, []); + // Added this effect to clean up when the component unmounts + useEffect(() => { + return () => { + // Clean up all providers when the component unmounts + providersMap.current.forEach((data) => { + if (data.binding) { + data.binding.destroy(); + } + data.provider.disconnect(); + data.yDoc.destroy(); + }); + providersMap.current.clear(); + }; + }, []); // Connection/disconnection effect useEffect(() => { socketRef.current?.connect() - + return () => { socketRef.current?.disconnect() } @@ -425,7 +436,7 @@ export default function CodeEditor({ // Socket event listener effect useEffect(() => { - const onConnect = () => {} + const onConnect = () => { } const onDisconnect = () => { setTerminals([]) @@ -530,8 +541,8 @@ export default function CodeEditor({ ? numTabs === 1 ? null : index < numTabs - 1 - ? tabs[index + 1].id - : tabs[index - 1].id + ? tabs[index + 1].id + : tabs[index - 1].id : activeFileId setTabs((prev) => prev.filter((t) => t.id !== id)) @@ -624,7 +635,7 @@ export default function CodeEditor({ {}} + setOpen={() => { }} /> @@ -633,216 +644,211 @@ export default function CodeEditor({ return ( <> {/* Copilot DOM elements */} -
-
- {generate.show && ai ? ( - t.id === activeFileId)?.name ?? "", - code: editorRef?.getValue() ?? "", - line: generate.line, - }} - editor={{ - language: editorLanguage, - }} - onExpand={() => { - editorRef?.changeViewZones(function (changeAccessor) { - changeAccessor.removeZone(generate.id) + +
+
+ {generate.show && ai ? ( + t.id === activeFileId)?.name ?? "", + code: editorRef?.getValue() ?? "", + line: generate.line, + }} + editor={{ + language: editorLanguage, + }} + onExpand={() => { + editorRef?.changeViewZones(function (changeAccessor) { + changeAccessor.removeZone(generate.id) - if (!generateRef.current) return - const id = changeAccessor.addZone({ - afterLineNumber: cursorLine, - heightInLines: 12, - domNode: generateRef.current, + if (!generateRef.current) return + const id = changeAccessor.addZone({ + afterLineNumber: cursorLine, + heightInLines: 12, + domNode: generateRef.current, + }) + setGenerate((prev) => { + return { ...prev, id } + }) }) + }} + onAccept={(code: string) => { + const line = generate.line setGenerate((prev) => { - return { ...prev, id } + return { + ...prev, + show: !prev.show, + } }) - }) - }} - onAccept={(code: string) => { - 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) - }} - onClose={() => { - setGenerate((prev) => { - return { - ...prev, - show: !prev.show, - } - }) - }} - /> - ) : null} -
+ 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} +
- {/* Main editor components */} - addNew(name, type, setFiles, sandboxData)} - deletingFolderId={deletingFolderId} - // AI Copilot Toggle - ai={ai} - setAi={setAi} - /> + {/* Main editor components */} + addNew(name, type, setFiles, sandboxData)} + deletingFolderId={deletingFolderId} + // AI Copilot Toggle + ai={ai} + setAi={setAi} + /> - {/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */} - - -
- {/* File tabs */} - {tabs.map((tab) => ( - { - selectFile(tab) - }} - onClose={() => closeTab(tab.id)} - > - {tab.name} - - ))} -
- {/* Monaco editor */} -
+ - {!activeFileId ? ( - <> -
- - No file selected. -
- - ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 - clerk.loaded ? ( - <> - {provider && userInfo ? ( - - ) : null} - { - if (value === activeFileContent) { - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: true } - : tab - ) - ) - } else { - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: false } - : tab - ) - ) - } +
+ {/* File tabs */} + {tabs.map((tab) => ( + { + selectFile(tab) }} - options={{ - tabSize: 2, - minimap: { - enabled: false, - }, - padding: { - bottom: 4, - top: 4, - }, - scrollBeyondLastLine: false, - fixedOverflowWidgets: true, - fontFamily: "var(--font-geist-mono)", - }} - theme="vs-dark" - value={activeFileContent} - /> - - ) : ( -
- - Waiting for Clerk to load... -
- )} -
-
- - - - setIsPreviewCollapsed(true)} - onExpand={() => setIsPreviewCollapsed(false)} + onClose={() => closeTab(tab.id)} + > + {tab.name} + + ))} +
+ {/* Monaco editor */} +
- { - previewPanelRef.current?.expand() - setIsPreviewCollapsed(false) - }} - src={previewURL} - /> - - - - {isOwner ? ( - - ) : ( -
- - No terminal access. -
- )} -
- - - + {!activeFileId ? ( + <> +
+ + No file selected. +
+ + ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 + clerk.loaded ? ( + <> + {provider && userInfo ? ( + + ) : null} + { + if (value === activeFileContent) { + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: true } + : tab + ) + ) + } else { + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: false } + : tab + ) + ) + } + }} + options={{ + tabSize: 2, + minimap: { + enabled: false, + }, + padding: { + bottom: 4, + top: 4, + }, + scrollBeyondLastLine: false, + fixedOverflowWidgets: true, + fontFamily: "var(--font-geist-mono)", + }} + theme="vs-dark" + value={activeFileContent} + /> + + ) : ( +
+ + Waiting for Clerk to load... +
+ )} +
+
+ + + + setIsPreviewCollapsed(true)} + onExpand={() => setIsPreviewCollapsed(false)} + > + { + usePreview().previewPanelRef.current?.expand() + setIsPreviewCollapsed(false) + } } collapsed={isPreviewCollapsed} src={previewURL}/> + + + + {isOwner ? ( + + ) : ( +
+ + No terminal access. +
+ )} +
+
+
+
+
) } diff --git a/frontend/components/editor/navbar/index.tsx b/frontend/components/editor/navbar/index.tsx index 413200b..652fa03 100644 --- a/frontend/components/editor/navbar/index.tsx +++ b/frontend/components/editor/navbar/index.tsx @@ -11,6 +11,8 @@ import { useState } from "react"; import EditSandboxModal from "./edit"; import ShareSandboxModal from "./share"; import { Avatars } from "../live/avatars"; +import RunButtonModal from "./run"; +import DeployButtonModal from "./deploy"; export default function Navbar({ userData, @@ -19,15 +21,13 @@ export default function Navbar({ }: { userData: User; sandboxData: Sandbox; - shared: { - id: string; - name: string; - }[]; + shared: { id: string; name: string }[]; }) { const [isEditOpen, setIsEditOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false); + const [isRunning, setIsRunning] = useState(false); - const isOwner = sandboxData.userId === userData.id; + const isOwner = sandboxData.userId === userData.id;; return ( <> @@ -62,18 +62,25 @@ export default function Navbar({ ) : null}
+
{isOwner ? ( + <> + + ) : null}
); -} +} \ No newline at end of file diff --git a/frontend/components/editor/navbar/run.tsx b/frontend/components/editor/navbar/run.tsx new file mode 100644 index 0000000..688886f --- /dev/null +++ b/frontend/components/editor/navbar/run.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Play, StopCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTerminal } from "@/context/TerminalContext"; +import { usePreview } from "@/context/PreviewContext"; +import { toast } from "sonner"; + +export default function RunButtonModal({ + isRunning, + setIsRunning, +}: { + isRunning: boolean; + setIsRunning: (running: boolean) => void; +}) { + const { createNewTerminal, terminals, closeTerminal } = useTerminal(); + const { setIsPreviewCollapsed, previewPanelRef} = usePreview(); + + const handleRun = () => { + if (isRunning) { + console.log('Stopping sandbox...'); + console.log('Closing Preview Window'); + + terminals.forEach(term => { + if (term.terminal) { + closeTerminal(term.id); + console.log('Closing Terminal', term.id); + } + }); + + setIsPreviewCollapsed(true); + previewPanelRef.current?.collapse(); + } else { + console.log('Running sandbox...'); + console.log('Opening Terminal'); + console.log('Opening Preview Window'); + + if (terminals.length < 4) { + createNewTerminal(); + } else { + toast.error("You reached the maximum # of terminals."); + console.error('Maximum number of terminals reached.'); + } + + setIsPreviewCollapsed(false); + previewPanelRef.current?.expand(); + } + setIsRunning(!isRunning); + }; + + return ( + <> + + + ); +} \ No newline at end of file diff --git a/frontend/components/editor/preview/index.tsx b/frontend/components/editor/preview/index.tsx index a400d14..940f8ba 100644 --- a/frontend/components/editor/preview/index.tsx +++ b/frontend/components/editor/preview/index.tsx @@ -1,13 +1,9 @@ "use client" import { - ChevronLeft, - ChevronRight, - Globe, Link, RotateCw, TerminalSquare, - UnfoldVertical, } from "lucide-react" import { useRef, useState } from "react" import { toast } from "sonner" @@ -27,22 +23,22 @@ export default function PreviewWindow({ return ( <>
Preview
{collapsed ? ( - - + { }}> + ) : ( <> - {/* Todo, make this open inspector */} - {/* {}}> - + {/* Removed the unfoldvertical button since we have the same thing via the run button. + + + */} {children} diff --git a/frontend/components/editor/terminals/index.tsx b/frontend/components/editor/terminals/index.tsx index a777eeb..3381b33 100644 --- a/frontend/components/editor/terminals/index.tsx +++ b/frontend/components/editor/terminals/index.tsx @@ -2,55 +2,62 @@ import { Button } from "@/components/ui/button"; import Tab from "@/components/ui/tab"; -import { closeTerminal, createTerminal } from "@/lib/terminal"; import { Terminal } from "@xterm/xterm"; import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"; -import { Socket } from "socket.io-client"; import { toast } from "sonner"; import EditorTerminal from "./terminal"; -import { useState } from "react"; +import { useTerminal } from "@/context/TerminalContext"; +import { useEffect } from "react"; + +export default function Terminals() { + const { + terminals, + setTerminals, + socket, + createNewTerminal, + closeTerminal, + activeTerminalId, + setActiveTerminalId, + creatingTerminal, + } = useTerminal(); -export default function Terminals({ - terminals, - setTerminals, - socket, -}: { - terminals: { id: string; terminal: Terminal | null }[]; - setTerminals: React.Dispatch< - React.SetStateAction< - { - id: string; - terminal: Terminal | null; - }[] - > - >; - socket: Socket; -}) { - const [activeTerminalId, setActiveTerminalId] = useState(""); - const [creatingTerminal, setCreatingTerminal] = useState(false); - const [closingTerminal, setClosingTerminal] = useState(""); const activeTerminal = terminals.find((t) => t.id === activeTerminalId); + // Effect to set the active terminal when a new one is created + useEffect(() => { + if (terminals.length > 0 && !activeTerminalId) { + setActiveTerminalId(terminals[terminals.length - 1].id); + } + }, [terminals, activeTerminalId, setActiveTerminalId]); + + const handleCreateTerminal = () => { + if (terminals.length >= 4) { + toast.error("You reached the maximum # of terminals."); + return; + } + createNewTerminal(); + }; + + const handleCloseTerminal = (termId: string) => { + closeTerminal(termId); + if (activeTerminalId === termId) { + const remainingTerminals = terminals.filter(t => t.id !== termId); + if (remainingTerminals.length > 0) { + setActiveTerminalId(remainingTerminals[0].id); + } else { + setActiveTerminalId(""); + } + } + }; + return ( <>
{terminals.map((term) => ( setActiveTerminalId(term.id)} - onClose={() => - closeTerminal({ - term, - terminals, - setTerminals, - setActiveTerminalId, - setClosingTerminal, - socket, - activeTerminalId, - }) - } - closing={closingTerminal === term.id} + onClose={() => handleCloseTerminal(term.id)} selected={activeTerminalId === term.id} > @@ -59,18 +66,7 @@ export default function Terminals({ ))}