Merge branch 'refs/heads/feat/run-deploy-buttons' into feat/dokku

# Conflicts:
#	backend/server/package-lock.json
#	backend/server/src/index.ts
#	frontend/components/editor/index.tsx
#	frontend/components/editor/navbar/deploy.tsx
#	frontend/components/editor/navbar/index.tsx
This commit is contained in:
James Murdza 2024-07-27 08:24:40 -04:00
commit 2e68b0b537
10 changed files with 516 additions and 299 deletions

View File

@ -6,6 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs" import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -27,7 +29,11 @@ export default function RootLayout({
forcedTheme="dark" forcedTheme="dark"
disableTransitionOnChange disableTransitionOnChange
> >
<PreviewProvider>
<TerminalProvider>
{children} {children}
</TerminalProvider>
</PreviewProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
</ThemeProvider> </ThemeProvider>

View File

@ -31,6 +31,8 @@ import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useTerminal } from '@/context/TerminalContext';
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -44,12 +46,21 @@ export default function CodeEditor({
// Initialize socket connection if it doesn't exist // Initialize socket connection if it doesn't exist
if (!socketRef.current) { if (!socketRef.current) {
socketRef.current = io( socketRef.current = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`, `${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_SERVER_PORT}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{ {
timeout: 2000, 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 [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [disableAccess, setDisableAccess] = useState({ const [disableAccess, setDisableAccess] = useState({
isDisabled: false, isDisabled: false,
@ -315,7 +326,7 @@ export default function CodeEditor({
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`); console.log(`Saving file...${value}`);
socketRef.current?.emit("saveFile", activeFileId, value); socketRef.current?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY)||1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef] [socketRef]
); );
@ -383,7 +394,6 @@ export default function CodeEditor({
); );
providerData.binding = binding; providerData.binding = binding;
setProvider(providerData.provider); setProvider(providerData.provider);
return () => { return () => {
@ -397,25 +407,24 @@ export default function CodeEditor({
}; };
}, [room, activeFileContent]); }, [room, activeFileContent]);
// Added this effect to clean up when the component unmounts // Added this effect to clean up when the component unmounts
useEffect(() => { useEffect(() => {
return () => { return () => {
// Clean up all providers when the component unmounts // Clean up all providers when the component unmounts
providersMap.current.forEach((data) => { providersMap.current.forEach((data) => {
if (data.binding) { if (data.binding) {
data.binding.destroy(); data.binding.destroy();
} }
data.provider.disconnect(); data.provider.disconnect();
data.yDoc.destroy(); data.yDoc.destroy();
}); });
providersMap.current.clear(); providersMap.current.clear();
}; };
}, []); }, []);
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
socketRef.current?.connect() socketRef.current?.connect()
return () => { return () => {
socketRef.current?.disconnect() socketRef.current?.disconnect()
} }
@ -423,7 +432,7 @@ export default function CodeEditor({
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => {} const onConnect = () => { }
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]) setTerminals([])
@ -528,8 +537,8 @@ export default function CodeEditor({
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)) setTabs((prev) => prev.filter((t) => t.id !== id))
@ -622,7 +631,7 @@ export default function CodeEditor({
<DisableAccessModal <DisableAccessModal
message={disableAccess.message} message={disableAccess.message}
open={disableAccess.isDisabled} open={disableAccess.isDisabled}
setOpen={() => {}} setOpen={() => { }}
/> />
<Loading /> <Loading />
</> </>
@ -631,216 +640,211 @@ export default function CodeEditor({
return ( return (
<> <>
{/* Copilot DOM elements */} {/* Copilot DOM elements */}
<div ref={generateRef} /> <PreviewProvider>
<div className="z-50 p-1" ref={generateWidgetRef}> <div ref={generateRef} />
{generate.show && ai ? ( <div className="z-50 p-1" ref={generateWidgetRef}>
<GenerateInput {generate.show && ai ? (
user={userData} <GenerateInput
socket={socketRef.current} user={userData}
width={generate.width - 90} socket={socketRef.current}
data={{ width={generate.width - 90}
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "", data={{
code: editorRef?.getValue() ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
line: generate.line, code: editorRef?.getValue() ?? "",
}} line: generate.line,
editor={{ }}
language: editorLanguage, editor={{
}} language: editorLanguage,
onExpand={() => { }}
editorRef?.changeViewZones(function (changeAccessor) { onExpand={() => {
changeAccessor.removeZone(generate.id) editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id)
if (!generateRef.current) return if (!generateRef.current) return
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 12, heightInLines: 12,
domNode: generateRef.current, domNode: generateRef.current,
})
setGenerate((prev) => {
return { ...prev, id }
})
}) })
}}
onAccept={(code: string) => {
const line = generate.line
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id } return {
...prev,
show: !prev.show,
}
}) })
}) const file = editorRef?.getValue()
}}
onAccept={(code: string) => {
const line = generate.line
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
}
})
const file = editorRef?.getValue()
const lines = file?.split("\n") || [] const lines = file?.split("\n") || []
lines.splice(line - 1, 0, code) lines.splice(line - 1, 0, code)
const updatedFile = lines.join("\n") const updatedFile = lines.join("\n")
editorRef?.setValue(updatedFile) editorRef?.setValue(updatedFile)
}} }}
onClose={() => { onClose={() => {
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
show: !prev.show, show: !prev.show,
} }
}) })
}} }}
/> />
) : null} ) : null}
</div> </div>
{/* Main editor components */} {/* Main editor components */}
<Sidebar <Sidebar
sandboxData={sandboxData} sandboxData={sandboxData}
files={files} files={files}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename} handleRename={handleRename}
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder} handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current} socket={socketRef.current}
setFiles={setFiles} setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)} addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId} deletingFolderId={deletingFolderId}
// AI Copilot Toggle // AI Copilot Toggle
ai={ai} ai={ai}
setAi={setAi} setAi={setAi}
/> />
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */} {/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
<ResizablePanelGroup direction="horizontal"> <ResizablePanelGroup direction="horizontal">
<ResizablePanel <ResizablePanel
className="p-2 flex flex-col" className="p-2 flex flex-col"
maxSize={80} maxSize={80}
minSize={30} minSize={30}
defaultSize={60} defaultSize={60}
ref={editorPanelRef} ref={editorPanelRef}
>
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
{/* File tabs */}
{tabs.map((tab) => (
<Tab
key={tab.id}
saved={tab.saved}
selected={activeFileId === tab.id}
onClick={(e) => {
selectFile(tab)
}}
onClose={() => closeTab(tab.id)}
>
{tab.name}
</Tab>
))}
</div>
{/* Monaco editor */}
<div
ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md relative"
> >
{!activeFileId ? ( <div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
<> {/* File tabs */}
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> {tabs.map((tab) => (
<FileJson className="w-6 h-6 mr-3" /> <Tab
No file selected. key={tab.id}
</div> saved={tab.saved}
</> selected={activeFileId === tab.id}
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 onClick={(e) => {
clerk.loaded ? ( selectFile(tab)
<>
{provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} />
) : null}
<Editor
height="100%"
language={editorLanguage}
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
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={{ onClose={() => closeTab(tab.id)}
tabSize: 2, >
minimap: { {tab.name}
enabled: false, </Tab>
}, ))}
padding: { </div>
bottom: 4, {/* Monaco editor */}
top: 4, <div
}, ref={editorContainerRef}
scrollBeyondLastLine: false, className="grow w-full overflow-hidden rounded-md relative"
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
theme="vs-dark"
value={activeFileContent}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<Loader2 className="animate-spin w-6 h-6 mr-3" />
Waiting for Clerk to load...
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
ref={previewPanelRef}
defaultSize={4}
collapsedSize={4}
minSize={25}
collapsible
className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
> >
<PreviewWindow {!activeFileId ? (
collapsed={isPreviewCollapsed} <>
open={() => { <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
previewPanelRef.current?.expand() <FileJson className="w-6 h-6 mr-3" />
setIsPreviewCollapsed(false) No file selected.
}} </div>
src={previewURL} </>
/> ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
</ResizablePanel> clerk.loaded ? (
<ResizableHandle /> <>
<ResizablePanel {provider && userInfo ? (
defaultSize={50} <Cursors yProvider={provider} userInfo={userInfo} />
minSize={20} ) : null}
className="p-2 flex flex-col" <Editor
> height="100%"
{isOwner ? ( language={editorLanguage}
<Terminals beforeMount={handleEditorWillMount}
terminals={terminals} onMount={handleEditorMount}
setTerminals={setTerminals} onChange={(value) => {
socket={socketRef.current} if (value === activeFileContent) {
/> setTabs((prev) =>
) : ( prev.map((tab) =>
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none"> tab.id === activeFileId
<TerminalSquare className="w-4 h-4 mr-2" /> ? { ...tab, saved: true }
No terminal access. : tab
</div> )
)} )
</ResizablePanel> } else {
</ResizablePanelGroup> setTabs((prev) =>
</ResizablePanel> prev.map((tab) =>
</ResizablePanelGroup> 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}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<Loader2 className="animate-spin w-6 h-6 mr-3" />
Waiting for Clerk to load...
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
ref={usePreview().previewPanelRef}
defaultSize={4}
collapsedSize={4}
minSize={25}
collapsible
className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
>
<PreviewWindow
open={() => {
usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
} } collapsed={isPreviewCollapsed} src={previewURL}/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
{isOwner ? (
<Terminals />
) : (
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
<TerminalSquare className="w-4 h-4 mr-2" />
No terminal access.
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</PreviewProvider>
</> </>
) )
} }

View File

@ -11,6 +11,7 @@ import { useState } from "react";
import EditSandboxModal from "./edit"; import EditSandboxModal from "./edit";
import ShareSandboxModal from "./share"; import ShareSandboxModal from "./share";
import { Avatars } from "../live/avatars"; import { Avatars } from "../live/avatars";
import RunButtonModal from "./run";
import DeployButtonModal from "./deploy"; import DeployButtonModal from "./deploy";
export default function Navbar({ export default function Navbar({
@ -20,15 +21,13 @@ export default function Navbar({
}: { }: {
userData: User; userData: User;
sandboxData: Sandbox; sandboxData: Sandbox;
shared: { shared: { id: string; name: string }[];
id: string;
name: string;
}[];
}) { }) {
const [isEditOpen, setIsEditOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false);
const [isShareOpen, setIsShareOpen] = 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 ( return (
<> <>
@ -63,16 +62,20 @@ export default function Navbar({
) : null} ) : null}
</div> </div>
</div> </div>
<RunButtonModal
isRunning={isRunning}
setIsRunning={setIsRunning}
/>
<div className="flex items-center h-full space-x-4"> <div className="flex items-center h-full space-x-4">
<Avatars /> <Avatars />
{isOwner ? ( {isOwner ? (
<> <>
<DeployButtonModal /> <DeployButtonModal />
<Button variant="outline" onClick={() => setIsShareOpen(true)}> <Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Share Share
</Button> </Button>
</> </>
) : null} ) : null}
<UserButton userData={userData} /> <UserButton userData={userData} />

View File

@ -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 (
<>
<Button variant="outline" onClick={handleRun}>
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isRunning ? 'Stop' : 'Run'}
</Button>
</>
);
}

View File

@ -1,13 +1,9 @@
"use client" "use client"
import { import {
ChevronLeft,
ChevronRight,
Globe,
Link, Link,
RotateCw, RotateCw,
TerminalSquare, TerminalSquare,
UnfoldVertical,
} from "lucide-react" } from "lucide-react"
import { useRef, useState } from "react" import { useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
@ -27,22 +23,22 @@ export default function PreviewWindow({
return ( return (
<> <>
<div <div
className={`${ className={`${collapsed ? "h-full" : "h-10"
collapsed ? "h-full" : "h-10" } select-none w-full flex gap-2`}
} select-none w-full flex gap-2`}
> >
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between"> <div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div> <div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1"> <div className="flex space-x-1 translate-x-1">
{collapsed ? ( {collapsed ? (
<PreviewButton onClick={open}> <PreviewButton disabled onClick={() => { }}>
<UnfoldVertical className="w-4 h-4" /> <TerminalSquare className="w-4 h-4" />
</PreviewButton> </PreviewButton>
) : ( ) : (
<> <>
{/* Todo, make this open inspector */} {/* Removed the unfoldvertical button since we have the same thing via the run button.
{/* <PreviewButton disabled onClick={() => {}}>
<TerminalSquare className="w-4 h-4" /> <PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton> */} </PreviewButton> */}
<PreviewButton <PreviewButton
@ -94,9 +90,8 @@ function PreviewButton({
}) { }) {
return ( return (
<div <div
className={`${ className={`${disabled ? "pointer-events-none opacity-50" : ""
disabled ? "pointer-events-none opacity-50" : "" } p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
onClick={onClick} onClick={onClick}
> >
{children} {children}

View File

@ -2,35 +2,42 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Tab from "@/components/ui/tab"; import Tab from "@/components/ui/tab";
import { closeTerminal, createTerminal } from "@/lib/terminal";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"; import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
import { Socket } from "socket.io-client";
import { toast } from "sonner"; import { toast } from "sonner";
import EditorTerminal from "./terminal"; 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); 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();
};
return ( return (
<> <>
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll"> <div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
@ -39,18 +46,7 @@ export default function Terminals({
key={term.id} key={term.id}
creating={creatingTerminal} creating={creatingTerminal}
onClick={() => setActiveTerminalId(term.id)} onClick={() => setActiveTerminalId(term.id)}
onClose={() => onClose={() => closeTerminal(term.id)}
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket,
activeTerminalId,
})
}
closing={closingTerminal === term.id}
selected={activeTerminalId === term.id} selected={activeTerminalId === term.id}
> >
<SquareTerminal className="w-4 h-4 mr-2" /> <SquareTerminal className="w-4 h-4 mr-2" />
@ -59,18 +55,7 @@ export default function Terminals({
))} ))}
<Button <Button
disabled={creatingTerminal} disabled={creatingTerminal}
onClick={() => { onClick={handleCreateTerminal}
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createTerminal({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
}}
size="smIcon" size="smIcon"
variant={"secondary"} variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`} className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}

View File

@ -74,6 +74,20 @@ export default function EditorTerminal({
}; };
}, [term, terminalRef.current]); }, [term, terminalRef.current]);
useEffect(() => {
if (!term) return;
const handleTerminalResponse = (response: { id: string; data: string }) => {
if (response.id === id) {
term.write(response.data);
}
};
socket.on("terminalResponse", handleTerminalResponse);
return () => {
socket.off("terminalResponse", handleTerminalResponse);
};
}, [term, id, socket]);
return ( return (
<> <>
<div <div

View File

@ -0,0 +1,34 @@
"use client"
import React, { createContext, useContext, useState, useRef } from 'react';
import { ImperativePanelHandle } from "react-resizable-panels";
interface PreviewContextType {
isPreviewCollapsed: boolean;
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
previewURL: string;
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
previewPanelRef: React.RefObject<ImperativePanelHandle>;
}
const PreviewContext = createContext<PreviewContextType | undefined>(undefined);
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
const [previewURL, setPreviewURL] = useState<string>("");
const previewPanelRef = useRef<ImperativePanelHandle>(null);
return (
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
{children}
</PreviewContext.Provider>
);
};
export const usePreview = () => {
const context = useContext(PreviewContext);
if (context === undefined) {
throw new Error('usePreview must be used within a PreviewProvider');
}
return context;
};

View File

@ -0,0 +1,117 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
creatingTerminal: boolean;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: () => void;
closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_SERVER_PORT}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const createNewTerminal = async () => {
if (!socket) return;
setCreatingTerminal(true);
try {
createTerminalHelper({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
} catch (error) {
console.error("Error creating terminal:", error);
} finally {
setCreatingTerminal(false);
}
};
const closeTerminal = (id: string) => {
if (!socket) return;
const terminalToClose = terminals.find(term => term.id === id);
if (terminalToClose) {
closeTerminalHelper({
term: terminalToClose,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal: () => {},
socket,
activeTerminalId,
});
}
};
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
terminals,
setTerminals,
activeTerminalId,
setActiveTerminalId,
creatingTerminal,
setCreatingTerminal,
createNewTerminal,
closeTerminal,
setUserAndSandboxId,
};
return (
<TerminalContext.Provider value={value}>
{children}
</TerminalContext.Provider>
);
};
export const useTerminal = (): TerminalContextType => {
const context = useContext(TerminalContext);
if (!context) {
throw new Error('useTerminal must be used within a TerminalProvider');
}
return context;
};