organize & comment code

This commit is contained in:
Ishaan Dey 2024-05-08 23:52:08 -07:00
parent 2ef5f85099
commit db8c26cd38
7 changed files with 327 additions and 202 deletions

View File

@ -6,7 +6,6 @@ import Editor, { BeforeMount, OnMount } from "@monaco-editor/react";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { toast } from "sonner"; import { toast } from "sonner";
import { useClerk } from "@clerk/nextjs"; import { useClerk } from "@clerk/nextjs";
import { createId } from "@paralleldrive/cuid2";
import * as Y from "yjs"; import * as Y from "yjs";
import LiveblocksProvider from "@liveblocks/yjs"; import LiveblocksProvider from "@liveblocks/yjs";
@ -19,38 +18,32 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable"; } from "@/components/ui/resizable";
import { import { FileJson, Loader2, TerminalSquare } from "lucide-react";
ChevronLeft,
ChevronRight,
FileJson,
Loader2,
Plus,
RotateCw,
Shell,
SquareTerminal,
TerminalSquare,
} from "lucide-react";
import Tab from "../ui/tab"; import Tab from "../ui/tab";
import Sidebar from "./sidebar"; import Sidebar from "./sidebar";
import EditorTerminal from "./terminal";
import { Button } from "../ui/button";
import GenerateInput from "./generate"; import GenerateInput from "./generate";
import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types"; import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types";
import { processFileType, validateName } from "@/lib/utils"; import { processFileType, validateName } from "@/lib/utils";
import { Cursors } from "./live/cursors"; import { Cursors } from "./live/cursors";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import DisableAccessModal from "./live/disableModal"; import DisableAccessModal from "./live/disableModal";
import Loading from "./loading"; import Loading from "./loading";
import PreviewWindow from "./preview";
import Terminals from "./terminals";
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
sandboxData, sandboxData,
isSharedUser, }: // isSharedUser,
}: { {
userData: User; userData: User;
sandboxData: Sandbox; sandboxData: Sandbox;
isSharedUser: boolean; isSharedUser: boolean;
}) { }) {
const socket = io(
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
);
const [files, setFiles] = useState<(TFolder | TFile)[]>([]); const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
const [tabs, setTabs] = useState<TTab[]>([]); const [tabs, setTabs] = useState<TTab[]>([]);
const [editorLanguage, setEditorLanguage] = useState("plaintext"); const [editorLanguage, setEditorLanguage] = useState("plaintext");
@ -90,7 +83,6 @@ export default function CodeEditor({
const room = useRoom(); const room = useRoom();
const activeTerminal = terminals.find((t) => t.id === activeTerminalId); const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const [editorRef, setEditorRef] = const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>(); useState<monaco.editor.IStandaloneCodeEditor>();
const editorContainerRef = useRef<HTMLDivElement>(null); const editorContainerRef = useRef<HTMLDivElement>(null);
@ -98,16 +90,27 @@ export default function CodeEditor({
const generateRef = useRef<HTMLDivElement>(null); const generateRef = useRef<HTMLDivElement>(null);
const generateWidgetRef = useRef<HTMLDivElement>(null); const generateWidgetRef = useRef<HTMLDivElement>(null);
// Resize observer tracks editor width for generate widget
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width } = entry.contentRect;
setGenerate((prev) => {
return { ...prev, width };
});
}
});
// Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => { const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([ monaco.editor.addKeybindingRules([
{ {
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG, keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
command: "null", command: "null",
// when: "textInputFocus",
}, },
]); ]);
}; };
// Post-mount editor keybindings and actions
const handleEditorMount: OnMount = (editor, monaco) => { const handleEditorMount: OnMount = (editor, monaco) => {
setEditorRef(editor); setEditorRef(editor);
monacoRef.current = monaco; monacoRef.current = monaco;
@ -167,6 +170,7 @@ export default function CodeEditor({
}); });
}; };
// Generate widget effect
useEffect(() => { useEffect(() => {
if (!ai) { if (!ai) {
setGenerate((prev) => { setGenerate((prev) => {
@ -239,6 +243,7 @@ export default function CodeEditor({
} }
}, [generate.show]); }, [generate.show]);
// Decorations effect for generate widget tips
useEffect(() => { useEffect(() => {
if (decorations.options.length === 0) { if (decorations.options.length === 0) {
decorations.instance?.clear(); decorations.instance?.clear();
@ -261,10 +266,7 @@ export default function CodeEditor({
} }
}, [decorations.options]); }, [decorations.options]);
const socket = io( // Save file keybinding logic effect
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
);
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
@ -286,15 +288,7 @@ export default function CodeEditor({
}; };
}, [tabs, activeFileId]); }, [tabs, activeFileId]);
const resizeObserver = new ResizeObserver((entries) => { // Liveblocks live collaboration setup effect
for (const entry of entries) {
const { width } = entry.contentRect;
setGenerate((prev) => {
return { ...prev, width };
});
}
});
useEffect(() => { useEffect(() => {
const tab = tabs.find((t) => t.id === activeFileId); const tab = tabs.find((t) => t.id === activeFileId);
const model = editorRef?.getModel(); const model = editorRef?.getModel();
@ -317,8 +311,6 @@ export default function CodeEditor({
}, 0); }, 0);
} }
} }
} else {
// Yjs content is not synchronized
} }
}; };
@ -341,7 +333,7 @@ export default function CodeEditor({
}; };
}, [editorRef, room, activeFileContent]); }, [editorRef, room, activeFileContent]);
// connection/disconnection effect + resizeobserver // Connection/disconnection effect + resizeobserver
useEffect(() => { useEffect(() => {
socket.connect(); socket.connect();
@ -352,23 +344,14 @@ export default function CodeEditor({
return () => { return () => {
socket.disconnect(); socket.disconnect();
resizeObserver.disconnect(); resizeObserver.disconnect();
// terminals.forEach((term) => {
// if (term.terminal) {
// term.terminal.dispose();
// }
// });
}; };
}, []); }, []);
// event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => { const onConnect = () => {};
console.log("connected");
};
const onDisconnect = () => { const onDisconnect = () => {
console.log("disconnected");
setTerminals([]); setTerminals([]);
}; };
@ -412,23 +395,9 @@ export default function CodeEditor({
// }, []); // }, []);
}, [terminals]); }, [terminals]);
// Helper functions: // Helper functions for tabs:
const createTerminal = () => {
setCreatingTerminal(true);
const id = createId();
console.log("creating terminal, id:", id);
setTerminals((prev) => [...prev, { id, terminal: null }]);
setActiveTerminalId(id);
setTimeout(() => {
socket.emit("createTerminal", id, () => {
setCreatingTerminal(false);
});
}, 1000);
};
// Select file and load content
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return; if (tab.id === activeFileId) return;
const exists = tabs.find((t) => t.id === tab.id); const exists = tabs.find((t) => t.id === tab.id);
@ -448,6 +417,7 @@ export default function CodeEditor({
setActiveFileId(tab.id); setActiveFileId(tab.id);
}; };
// Close tab and remove from tabs
const closeTab = (tab: TFile) => { const closeTab = (tab: TFile) => {
const numTabs = tabs.length; const numTabs = tabs.length;
const index = tabs.findIndex((t) => t.id === tab.id); const index = tabs.findIndex((t) => t.id === tab.id);
@ -475,40 +445,6 @@ export default function CodeEditor({
} }
}; };
const closeTerminal = (term: { id: string; terminal: Terminal | null }) => {
const numTerminals = terminals.length;
const index = terminals.findIndex((t) => t.id === term.id);
if (index === -1) return;
setClosingTerminal(term.id);
socket.emit("closeTerminal", term.id, () => {
setClosingTerminal("");
const nextId =
activeTerminalId === term.id
? numTerminals === 1
? null
: index < numTerminals - 1
? terminals[index + 1].id
: terminals[index - 1].id
: activeTerminalId;
// if (activeTerminal && activeTerminal.terminal)
// activeTerminal.terminal.dispose();
setTerminals((prev) => prev.filter((t) => t.id !== term.id));
if (!nextId) {
setActiveTerminalId("");
} else {
const nextTerminal = terminals.find((t) => t.id === nextId);
if (nextTerminal) {
setActiveTerminalId(nextTerminal.id);
}
}
});
};
const handleRename = ( const handleRename = (
id: string, id: string,
newName: string, newName: string,
@ -542,7 +478,8 @@ export default function CodeEditor({
// }) // })
}; };
if (disableAccess.isDisabled) { // On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled)
return ( return (
<> <>
<DisableAccessModal <DisableAccessModal
@ -553,10 +490,10 @@ export default function CodeEditor({
<Loading /> <Loading />
</> </>
); );
}
return ( return (
<> <>
{/* Copilot DOM elements */}
<div ref={generateRef} /> <div ref={generateRef} />
<div className="z-50 p-1" ref={generateWidgetRef}> <div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? ( {generate.show && ai ? (
@ -606,6 +543,7 @@ export default function CodeEditor({
) : null} ) : null}
</div> </div>
{/* Main editor components */}
<Sidebar <Sidebar
files={files} files={files}
selectFile={selectFile} selectFile={selectFile}
@ -624,9 +562,12 @@ export default function CodeEditor({
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }]) // setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
} }
}} }}
// AI Copilot Toggle
ai={ai} ai={ai}
setAi={setAi} setAi={setAi}
/> />
{/* 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"
@ -635,6 +576,7 @@ export default function CodeEditor({
defaultSize={60} defaultSize={60}
> >
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll"> <div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
{/* File tabs */}
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab <Tab
key={tab.id} key={tab.id}
@ -649,6 +591,7 @@ export default function CodeEditor({
</Tab> </Tab>
))} ))}
</div> </div>
{/* Monaco editor */}
<div <div
ref={editorContainerRef} ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md relative" className="grow w-full overflow-hidden rounded-md relative"
@ -660,7 +603,8 @@ export default function CodeEditor({
No file selected. No file selected.
</div> </div>
</> </>
) : clerk.loaded ? ( ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? (
<> <>
{provider ? <Cursors yProvider={provider} /> : null} {provider ? <Cursors yProvider={provider} /> : null}
<Editor <Editor
@ -719,26 +663,7 @@ export default function CodeEditor({
minSize={20} minSize={20}
className="p-2 flex flex-col" className="p-2 flex flex-col"
> >
<div className="h-10 select-none w-full flex gap-2"> <PreviewWindow />
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
Preview
<div className="flex space-x-1 translate-x-1">
<div className="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">
<TerminalSquare className="w-4 h-4" />
</div>
<div className="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">
<ChevronLeft className="w-4 h-4" />
</div>
<div className="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">
<ChevronRight className="w-4 h-4" />
</div>
<div className="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">
<RotateCw className="w-3 h-3" />
</div>
</div>
</div>
</div>
<div className="w-full grow rounded-md bg-foreground"></div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel
@ -747,74 +672,18 @@ export default function CodeEditor({
className="p-2 flex flex-col" className="p-2 flex flex-col"
> >
{isOwner ? ( {isOwner ? (
<> <Terminals
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll"> terminals={terminals}
{terminals.map((term) => ( setTerminals={setTerminals}
<Tab activeTerminalId={activeTerminalId}
key={term.id} setActiveTerminalId={setActiveTerminalId}
onClick={() => setActiveTerminalId(term.id)} socket={socket}
onClose={() => closeTerminal(term)} activeTerminal={activeTerminal}
selected={activeTerminalId === term.id} creatingTerminal={creatingTerminal}
> setCreatingTerminal={setCreatingTerminal}
<SquareTerminal className="w-4 h-4 mr-2" /> closingTerminal={closingTerminal}
Shell setClosingTerminal={setClosingTerminal}
</Tab> />
))}
<Button
disabled={creatingTerminal}
onClick={() => {
if (terminals.length >= 4) {
toast.error(
"You reached the maximum # of terminals."
);
return;
}
createTerminal();
}}
size="smIcon"
variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground`}
>
{creatingTerminal ? (
<Loader2 className="animate-spin w-4 h-4" />
) : (
<Plus className="w-4 h-4" />
)}
</Button>
</div>
{socket && activeTerminal ? (
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
{terminals.map((term) => (
<EditorTerminal
key={term.id}
socket={socket}
id={term.id}
term={term.terminal}
setTerm={(t: Terminal) => {
// console.log(
// "setting terminal",
// activeTerminalId,
// t.options
// );
setTerminals((prev) =>
prev.map((term) =>
term.id === activeTerminalId
? { ...term, terminal: t }
: term
)
);
}}
visible={activeTerminalId === term.id}
/>
))}
</div>
) : (
<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 terminals open.
</div>
)}
</>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none"> <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" /> <TerminalSquare className="w-4 h-4 mr-2" />

View File

@ -0,0 +1,35 @@
"use client";
import {
ChevronLeft,
ChevronRight,
RotateCw,
TerminalSquare,
} from "lucide-react";
export default function PreviewWindow() {
return (
<>
<div className="h-10 select-none w-full flex gap-2">
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
Preview
<div className="flex space-x-1 translate-x-1">
<div className="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">
<TerminalSquare className="w-4 h-4" />
</div>
<div className="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">
<ChevronLeft className="w-4 h-4" />
</div>
<div className="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">
<ChevronRight className="w-4 h-4" />
</div>
<div className="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">
<RotateCw className="w-3 h-3" />
</div>
</div>
</div>
</div>
<div className="w-full grow rounded-md bg-foreground"></div>
</>
);
}

View File

@ -0,0 +1,126 @@
"use client";
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";
export default function Terminals({
terminals,
setTerminals,
activeTerminalId,
setActiveTerminalId,
socket,
activeTerminal,
creatingTerminal,
setCreatingTerminal,
closingTerminal,
setClosingTerminal,
}: {
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<
React.SetStateAction<
{
id: string;
terminal: Terminal | null;
}[]
>
>;
activeTerminalId: string;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
socket: Socket;
activeTerminal:
| {
id: string;
terminal: Terminal | null;
}
| undefined;
creatingTerminal: boolean;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
closingTerminal: string;
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>;
}) {
return (
<>
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
{terminals.map((term) => (
<Tab
key={term.id}
onClick={() => setActiveTerminalId(term.id)}
onClose={() =>
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket,
activeTerminalId,
})
}
closing={closingTerminal === term.id}
selected={activeTerminalId === term.id}
>
<SquareTerminal className="w-4 h-4 mr-2" />
Shell
</Tab>
))}
<Button
disabled={creatingTerminal}
onClick={() => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createTerminal({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
}}
size="smIcon"
variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground`}
>
{creatingTerminal ? (
<Loader2 className="animate-spin w-4 h-4" />
) : (
<Plus className="w-4 h-4" />
)}
</Button>
</div>
{socket && activeTerminal ? (
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
{terminals.map((term) => (
<EditorTerminal
key={term.id}
socket={socket}
id={term.id}
term={term.terminal}
setTerm={(t: Terminal) => {
setTerminals((prev) =>
prev.map((term) =>
term.id === activeTerminalId
? { ...term, terminal: t }
: term
)
);
}}
visible={activeTerminalId === term.id}
/>
))}
</div>
) : (
<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 terminals open.
</div>
)}
</>
);
}

View File

@ -1,8 +1,8 @@
"use client" "use client";
import { X } from "lucide-react" import { Loader2, X } from "lucide-react";
import { Button } from "./button" import { Button } from "./button";
import { MouseEvent, MouseEventHandler, useEffect } from "react" import { MouseEvent, MouseEventHandler, useEffect } from "react";
export default function Tab({ export default function Tab({
children, children,
@ -10,12 +10,14 @@ export default function Tab({
selected = false, selected = false,
onClick, onClick,
onClose, onClose,
closing = false,
}: { }: {
children: React.ReactNode children: React.ReactNode;
saved?: boolean saved?: boolean;
selected?: boolean selected?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement> onClick?: MouseEventHandler<HTMLButtonElement>;
onClose?: () => void onClose?: () => void;
closing?: boolean;
}) { }) {
return ( return (
<Button <Button
@ -31,17 +33,19 @@ export default function Tab({
{children} {children}
<div <div
onClick={ onClick={
onClose onClose && !closing
? (e) => { ? (e) => {
e.stopPropagation() e.stopPropagation();
e.preventDefault() e.preventDefault();
onClose() onClose();
} }
: undefined : undefined
} }
className="h-5 w-5 ml-0.5 group flex items-center justify-center translate-x-1 transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm" className="h-5 w-5 ml-0.5 group flex items-center justify-center translate-x-1 transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"
> >
{saved ? ( {closing ? (
<Loader2 className="animate-spin w-3 h-3" />
) : saved ? (
<X className="w-3 h-3" /> <X className="w-3 h-3" />
) : ( ) : (
<> <>
@ -51,5 +55,5 @@ export default function Tab({
)} )}
</div> </div>
</Button> </Button>
) );
} }

91
frontend/lib/terminal.ts Normal file
View File

@ -0,0 +1,91 @@
// Helper functions for terminal instances
import { createId } from "@paralleldrive/cuid2";
import { Terminal } from "@xterm/xterm";
import { Socket } from "socket.io-client";
export const createTerminal = ({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
}: {
setTerminals: React.Dispatch<React.SetStateAction<{
id: string;
terminal: Terminal | null;
}[]>>;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
socket: Socket;
}) => {
setCreatingTerminal(true);
const id = createId();
console.log("creating terminal, id:", id);
setTerminals((prev) => [...prev, { id, terminal: null }]);
setActiveTerminalId(id);
setTimeout(() => {
socket.emit("createTerminal", id, () => {
setCreatingTerminal(false);
});
}, 1000);
};
export const closeTerminal = ({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket,
activeTerminalId,
} : {
term: {
id: string;
terminal: Terminal | null
}
terminals: {
id: string;
terminal: Terminal | null
}[]
setTerminals: React.Dispatch<React.SetStateAction<{
id: string;
terminal: Terminal | null
}[]>>
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
socket: Socket
activeTerminalId: string
}) => {
const numTerminals = terminals.length;
const index = terminals.findIndex((t) => t.id === term.id);
if (index === -1) return;
setClosingTerminal(term.id);
socket.emit("closeTerminal", term.id, () => {
setClosingTerminal("");
const nextId =
activeTerminalId === term.id
? numTerminals === 1
? null
: index < numTerminals - 1
? terminals[index + 1].id
: terminals[index - 1].id
: activeTerminalId;
setTerminals((prev) => prev.filter((t) => t.id !== term.id));
if (!nextId) {
setActiveTerminalId("");
} else {
const nextTerminal = terminals.find((t) => t.id === nextId);
if (nextTerminal) {
setActiveTerminalId(nextTerminal.id);
}
}
});
};