Merge branch 'refs/heads/feat/dokku' into production
# Conflicts: # frontend/app/layout.tsx
This commit is contained in:
@ -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,
|
||||
@ -48,8 +50,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,
|
||||
@ -315,7 +326,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]
|
||||
);
|
||||
|
||||
@ -341,7 +352,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();
|
||||
@ -383,7 +394,6 @@ export default function CodeEditor({
|
||||
);
|
||||
|
||||
providerData.binding = binding;
|
||||
|
||||
setProvider(providerData.provider);
|
||||
|
||||
return () => {
|
||||
@ -397,25 +407,24 @@ 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()
|
||||
}
|
||||
@ -423,7 +432,7 @@ export default function CodeEditor({
|
||||
|
||||
// Socket event listener effect
|
||||
useEffect(() => {
|
||||
const onConnect = () => {}
|
||||
const onConnect = () => { }
|
||||
|
||||
const onDisconnect = () => {
|
||||
setTerminals([])
|
||||
@ -528,8 +537,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))
|
||||
@ -622,7 +631,7 @@ export default function CodeEditor({
|
||||
<DisableAccessModal
|
||||
message={disableAccess.message}
|
||||
open={disableAccess.isDisabled}
|
||||
setOpen={() => {}}
|
||||
setOpen={() => { }}
|
||||
/>
|
||||
<Loading />
|
||||
</>
|
||||
@ -631,216 +640,211 @@ export default function CodeEditor({
|
||||
return (
|
||||
<>
|
||||
{/* Copilot DOM elements */}
|
||||
<div ref={generateRef} />
|
||||
<div className="z-50 p-1" ref={generateWidgetRef}>
|
||||
{generate.show && ai ? (
|
||||
<GenerateInput
|
||||
user={userData}
|
||||
socket={socketRef.current}
|
||||
width={generate.width - 90}
|
||||
data={{
|
||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||
code: editorRef?.getValue() ?? "",
|
||||
line: generate.line,
|
||||
}}
|
||||
editor={{
|
||||
language: editorLanguage,
|
||||
}}
|
||||
onExpand={() => {
|
||||
editorRef?.changeViewZones(function (changeAccessor) {
|
||||
changeAccessor.removeZone(generate.id)
|
||||
<PreviewProvider>
|
||||
<div ref={generateRef} />
|
||||
<div className="z-50 p-1" ref={generateWidgetRef}>
|
||||
{generate.show && ai ? (
|
||||
<GenerateInput
|
||||
user={userData}
|
||||
socket={socketRef.current}
|
||||
width={generate.width - 90}
|
||||
data={{
|
||||
fileName: tabs.find((t) => 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}
|
||||
</div>
|
||||
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}
|
||||
</div>
|
||||
|
||||
{/* Main editor components */}
|
||||
<Sidebar
|
||||
sandboxData={sandboxData}
|
||||
files={files}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
socket={socketRef.current}
|
||||
setFiles={setFiles}
|
||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||
deletingFolderId={deletingFolderId}
|
||||
// AI Copilot Toggle
|
||||
ai={ai}
|
||||
setAi={setAi}
|
||||
/>
|
||||
{/* Main editor components */}
|
||||
<Sidebar
|
||||
sandboxData={sandboxData}
|
||||
files={files}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
socket={socketRef.current}
|
||||
setFiles={setFiles}
|
||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||
deletingFolderId={deletingFolderId}
|
||||
// AI Copilot Toggle
|
||||
ai={ai}
|
||||
setAi={setAi}
|
||||
/>
|
||||
|
||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel
|
||||
className="p-2 flex flex-col"
|
||||
maxSize={80}
|
||||
minSize={30}
|
||||
defaultSize={60}
|
||||
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"
|
||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel
|
||||
className="p-2 flex flex-col"
|
||||
maxSize={80}
|
||||
minSize={30}
|
||||
defaultSize={60}
|
||||
ref={editorPanelRef}
|
||||
>
|
||||
{!activeFileId ? (
|
||||
<>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<FileJson className="w-6 h-6 mr-3" />
|
||||
No file selected.
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{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
|
||||
)
|
||||
)
|
||||
}
|
||||
<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)
|
||||
}}
|
||||
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={previewPanelRef}
|
||||
defaultSize={4}
|
||||
collapsedSize={4}
|
||||
minSize={25}
|
||||
collapsible
|
||||
className="p-2 flex flex-col"
|
||||
onCollapse={() => setIsPreviewCollapsed(true)}
|
||||
onExpand={() => setIsPreviewCollapsed(false)}
|
||||
onClose={() => closeTab(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
{/* Monaco editor */}
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className="grow w-full overflow-hidden rounded-md relative"
|
||||
>
|
||||
<PreviewWindow
|
||||
collapsed={isPreviewCollapsed}
|
||||
open={() => {
|
||||
previewPanelRef.current?.expand()
|
||||
setIsPreviewCollapsed(false)
|
||||
}}
|
||||
src={previewURL}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
defaultSize={50}
|
||||
minSize={20}
|
||||
className="p-2 flex flex-col"
|
||||
>
|
||||
{isOwner ? (
|
||||
<Terminals
|
||||
terminals={terminals}
|
||||
setTerminals={setTerminals}
|
||||
socket={socketRef.current}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
{!activeFileId ? (
|
||||
<>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<FileJson className="w-6 h-6 mr-3" />
|
||||
No file selected.
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{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={{
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
28
frontend/components/editor/navbar/deploy.tsx
Normal file
28
frontend/components/editor/navbar/deploy.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Play, Pause } from "lucide-react";
|
||||
|
||||
export default function DeployButtonModal() {
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
|
||||
const handleDeploy = () => {
|
||||
if (isDeploying) {
|
||||
console.log("Stopping deployment...");
|
||||
|
||||
} else {
|
||||
console.log("Starting deployment...");
|
||||
}
|
||||
setIsDeploying(!isDeploying);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleDeploy}>
|
||||
{isDeploying ? <Pause className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
|
||||
{isDeploying ? "Deployed" : "Deploy"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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}
|
||||
</div>
|
||||
</div>
|
||||
<RunButtonModal
|
||||
isRunning={isRunning}
|
||||
setIsRunning={setIsRunning}
|
||||
/>
|
||||
<div className="flex items-center h-full space-x-4">
|
||||
<Avatars />
|
||||
|
||||
{isOwner ? (
|
||||
<>
|
||||
<DeployButtonModal />
|
||||
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<UserButton userData={userData} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
59
frontend/components/editor/navbar/run.tsx
Normal file
59
frontend/components/editor/navbar/run.tsx
Normal 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("yarn install && yarn start");
|
||||
} 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<>
|
||||
<div
|
||||
className={`${
|
||||
collapsed ? "h-full" : "h-10"
|
||||
} select-none w-full flex gap-2`}
|
||||
className={`${collapsed ? "h-full" : "h-10"
|
||||
} 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="text-xs">Preview</div>
|
||||
<div className="flex space-x-1 translate-x-1">
|
||||
{collapsed ? (
|
||||
<PreviewButton onClick={open}>
|
||||
<UnfoldVertical className="w-4 h-4" />
|
||||
<PreviewButton disabled onClick={() => { }}>
|
||||
<TerminalSquare className="w-4 h-4" />
|
||||
</PreviewButton>
|
||||
) : (
|
||||
<>
|
||||
{/* Todo, make this open inspector */}
|
||||
{/* <PreviewButton disabled onClick={() => {}}>
|
||||
<TerminalSquare className="w-4 h-4" />
|
||||
{/* Removed the unfoldvertical button since we have the same thing via the run button.
|
||||
|
||||
<PreviewButton onClick={open}>
|
||||
<UnfoldVertical className="w-4 h-4" />
|
||||
</PreviewButton> */}
|
||||
|
||||
<PreviewButton
|
||||
@ -94,9 +90,8 @@ function PreviewButton({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
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`}
|
||||
className={`${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`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
|
@ -2,35 +2,42 @@
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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}
|
||||
creating={creatingTerminal}
|
||||
onClick={() => setActiveTerminalId(term.id)}
|
||||
onClose={() =>
|
||||
closeTerminal({
|
||||
term,
|
||||
terminals,
|
||||
setTerminals,
|
||||
setActiveTerminalId,
|
||||
setClosingTerminal,
|
||||
socket,
|
||||
activeTerminalId,
|
||||
})
|
||||
}
|
||||
closing={closingTerminal === term.id}
|
||||
onClose={() => closeTerminal(term.id)}
|
||||
selected={activeTerminalId === term.id}
|
||||
>
|
||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||
@ -59,18 +55,7 @@ export default function Terminals({
|
||||
))}
|
||||
<Button
|
||||
disabled={creatingTerminal}
|
||||
onClick={() => {
|
||||
if (terminals.length >= 4) {
|
||||
toast.error("You reached the maximum # of terminals.");
|
||||
return;
|
||||
}
|
||||
createTerminal({
|
||||
setTerminals,
|
||||
setActiveTerminalId,
|
||||
setCreatingTerminal,
|
||||
socket,
|
||||
});
|
||||
}}
|
||||
onClick={handleCreateTerminal}
|
||||
size="smIcon"
|
||||
variant={"secondary"}
|
||||
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
|
||||
@ -111,4 +96,4 @@ export default function Terminals({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -55,7 +55,6 @@ export default function EditorTerminal({
|
||||
fitAddon.fit();
|
||||
|
||||
const disposableOnData = term.onData((data) => {
|
||||
console.log("terminalData", id, data);
|
||||
socket.emit("terminalData", id, data);
|
||||
});
|
||||
|
||||
@ -74,6 +73,20 @@ export default function EditorTerminal({
|
||||
};
|
||||
}, [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 (
|
||||
<>
|
||||
<div
|
||||
|
Reference in New Issue
Block a user