781 lines
24 KiB
TypeScript
Raw Normal View History

"use client";
2024-04-06 19:03:04 -04:00
import { useEffect, useRef, useState } from "react";
import monaco from "monaco-editor";
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react";
import { io } from "socket.io-client";
import { toast } from "sonner";
import { useClerk } from "@clerk/nextjs";
2024-05-06 23:34:45 -07:00
import { createId } from "@paralleldrive/cuid2";
2024-05-03 13:53:21 -07:00
import * as Y from "yjs";
import LiveblocksProvider from "@liveblocks/yjs";
import { MonacoBinding } from "y-monaco";
import { Awareness } from "y-protocols/awareness";
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config";
2024-04-06 19:03:04 -04:00
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
2024-04-08 23:40:42 -04:00
import {
ChevronLeft,
ChevronRight,
2024-04-27 11:08:19 -04:00
FileJson,
2024-05-05 14:33:09 -07:00
Loader2,
2024-04-30 02:00:50 -04:00
Plus,
2024-04-08 23:40:42 -04:00
RotateCw,
2024-04-30 02:00:50 -04:00
Shell,
SquareTerminal,
2024-04-08 23:40:42 -04:00
TerminalSquare,
} from "lucide-react";
import Tab from "../ui/tab";
import Sidebar from "./sidebar";
import EditorTerminal from "./terminal";
import { Button } from "../ui/button";
import GenerateInput from "./generate";
import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types";
import { processFileType, validateName } from "@/lib/utils";
import { Cursors } from "./live/cursors";
import { Terminal } from "@xterm/xterm";
2024-04-28 20:06:47 -04:00
2024-04-26 22:34:56 -04:00
export default function CodeEditor({
2024-05-01 01:53:49 -04:00
userData,
2024-05-05 00:06:10 -07:00
sandboxData,
isSharedUser,
2024-04-26 22:34:56 -04:00
}: {
userData: User;
sandboxData: Sandbox;
isSharedUser: boolean;
2024-04-26 22:34:56 -04:00
}) {
const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
const [tabs, setTabs] = useState<TTab[]>([]);
const [editorLanguage, setEditorLanguage] = useState("plaintext");
const [activeFileId, setActiveFileId] = useState<string>("");
2024-05-06 23:34:45 -07:00
const [activeFileContent, setActiveFileContent] = useState("");
const [cursorLine, setCursorLine] = useState(0);
2024-05-02 17:38:37 -07:00
const [generate, setGenerate] = useState<{
show: boolean;
id: string;
line: number;
widget: monaco.editor.IContentWidget | undefined;
pref: monaco.editor.ContentWidgetPositionPreference[];
width: number;
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 });
2024-05-02 15:25:24 -07:00
const [decorations, setDecorations] = useState<{
options: monaco.editor.IModelDeltaDecoration[];
instance: monaco.editor.IEditorDecorationsCollection | undefined;
}>({ options: [], instance: undefined });
const [terminals, setTerminals] = useState<
{
id: string;
terminal: Terminal | null;
}[]
>([]);
2024-05-06 23:34:45 -07:00
const [activeTerminalId, setActiveTerminalId] = useState("");
const [creatingTerminal, setCreatingTerminal] = useState(false);
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
const [ai, setAi] = useState(false);
const isOwner = sandboxData.userId === userData.id;
const clerk = useClerk();
const room = useRoom();
2024-05-06 23:34:45 -07:00
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
console.log("activeTerminal", activeTerminal ? activeTerminal.id : "none");
2024-04-28 20:06:47 -04:00
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>();
const editorContainerRef = useRef<HTMLDivElement>(null);
const monacoRef = useRef<typeof monaco | null>(null);
const generateRef = useRef<HTMLDivElement>(null);
const generateWidgetRef = useRef<HTMLDivElement>(null);
2024-05-02 00:00:35 -07:00
const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([
{
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
command: "null",
// when: "textInputFocus",
},
]);
};
2024-04-06 19:03:04 -04:00
2024-04-27 00:20:17 -04:00
const handleEditorMount: OnMount = (editor, monaco) => {
setEditorRef(editor);
monacoRef.current = monaco;
2024-05-02 00:00:35 -07:00
editor.onDidChangeCursorPosition((e) => {
const { column, lineNumber } = e.position;
if (lineNumber === cursorLine) return;
setCursorLine(lineNumber);
2024-05-02 15:25:24 -07:00
const model = editor.getModel();
const endColumn = model?.getLineContent(lineNumber).length || 0;
2024-05-02 15:25:24 -07:00
setDecorations((prev) => {
return {
...prev,
options: [
{
range: new monaco.Range(
lineNumber,
column,
lineNumber,
endColumn
),
options: {
afterContentClassName: "inline-decoration",
},
},
],
};
});
});
2024-05-02 00:00:35 -07:00
2024-05-02 17:38:37 -07:00
editor.onDidBlurEditorText((e) => {
setDecorations((prev) => {
return {
...prev,
options: [],
};
});
});
2024-05-02 17:38:37 -07:00
2024-05-02 00:00:35 -07:00
editor.addAction({
id: "generate",
label: "Generate",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG],
precondition:
"editorTextFocus && !suggestWidgetVisible && !renameInputVisible && !inSnippetMode && !quickFixWidgetVisible",
2024-05-02 15:25:24 -07:00
run: () => {
setGenerate((prev) => {
2024-05-02 17:38:37 -07:00
return {
...prev,
show: !prev.show,
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
};
});
2024-05-02 15:25:24 -07:00
},
});
};
2024-04-06 19:03:04 -04:00
2024-05-02 00:00:35 -07:00
useEffect(() => {
2024-05-04 01:50:33 -07:00
if (!ai) {
setGenerate((prev) => {
return {
...prev,
show: false,
};
});
return;
2024-05-04 01:50:33 -07:00
}
2024-05-02 15:25:24 -07:00
if (generate.show) {
editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return;
2024-05-02 00:00:35 -07:00
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 3,
domNode: generateRef.current,
});
2024-05-02 15:25:24 -07:00
setGenerate((prev) => {
return { ...prev, id, line: cursorLine };
});
});
2024-05-02 17:38:37 -07:00
if (!generateWidgetRef.current) return;
const widgetElement = generateWidgetRef.current;
2024-05-02 17:38:37 -07:00
const contentWidget = {
getDomNode: () => {
return widgetElement;
2024-05-02 17:38:37 -07:00
},
getId: () => {
return "generate.widget";
2024-05-02 17:38:37 -07:00
},
getPosition: () => {
return {
position: {
lineNumber: cursorLine,
column: 1,
},
preference: generate.pref,
};
2024-05-02 17:38:37 -07:00
},
};
2024-05-02 17:38:37 -07:00
setGenerate((prev) => {
return { ...prev, widget: contentWidget };
});
editorRef?.addContentWidget(contentWidget);
2024-05-02 17:38:37 -07:00
if (generateRef.current && generateWidgetRef.current) {
editorRef?.applyFontInfo(generateRef.current);
editorRef?.applyFontInfo(generateWidgetRef.current);
2024-05-02 17:38:37 -07:00
}
2024-05-02 00:00:35 -07:00
} else {
editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id);
2024-05-02 15:25:24 -07:00
setGenerate((prev) => {
return { ...prev, id: "" };
});
});
2024-05-02 17:38:37 -07:00
if (!generate.widget) return;
editorRef?.removeContentWidget(generate.widget);
2024-05-02 17:38:37 -07:00
setGenerate((prev) => {
return {
...prev,
widget: undefined,
};
});
2024-05-02 15:25:24 -07:00
}
}, [generate.show]);
2024-05-02 15:25:24 -07:00
useEffect(() => {
2024-05-02 17:38:37 -07:00
if (decorations.options.length === 0) {
decorations.instance?.clear();
2024-05-02 17:38:37 -07:00
}
2024-05-02 15:25:24 -07:00
if (!ai) return;
2024-05-04 01:50:33 -07:00
2024-05-02 15:25:24 -07:00
if (decorations.instance) {
decorations.instance.set(decorations.options);
2024-05-02 15:25:24 -07:00
} else {
const instance = editorRef?.createDecorationsCollection();
instance?.set(decorations.options);
2024-05-02 15:25:24 -07:00
setDecorations((prev) => {
return {
...prev,
instance,
};
});
2024-05-02 00:00:35 -07:00
}
}, [decorations.options]);
2024-04-26 22:34:56 -04:00
const socket = io(
2024-05-05 00:06:10 -07:00
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
);
2024-04-26 22:34:56 -04:00
2024-04-27 16:41:25 -04:00
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
2024-04-27 16:41:25 -04:00
// const activeTab = tabs.find((t) => t.id === activeFileId)
// console.log("saving:", activeTab?.name, editorRef?.getValue())
2024-04-27 16:41:25 -04:00
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab
2024-04-27 16:41:25 -04:00
)
);
2024-04-27 16:41:25 -04:00
socket.emit("saveFile", activeFileId, editorRef?.getValue());
2024-04-27 16:41:25 -04:00
}
};
document.addEventListener("keydown", down);
2024-04-27 16:41:25 -04:00
return () => {
document.removeEventListener("keydown", down);
};
}, [tabs, activeFileId]);
2024-04-27 16:41:25 -04:00
2024-05-02 17:38:37 -07:00
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width } = entry.contentRect;
2024-05-02 17:38:37 -07:00
setGenerate((prev) => {
return { ...prev, width };
});
2024-05-02 17:38:37 -07:00
}
});
2024-04-27 16:41:25 -04:00
useEffect(() => {
const tab = tabs.find((t) => t.id === activeFileId);
const model = editorRef?.getModel();
if (!editorRef || !tab || !model) return;
const yDoc = new Y.Doc();
const yText = yDoc.getText(tab.id);
const yProvider: any = new LiveblocksProvider(room, yDoc);
const onSync = (isSynced: boolean) => {
if (isSynced) {
const text = yText.toString();
if (text === "") {
if (activeFileContent) {
yText.insert(0, activeFileContent);
} else {
setTimeout(() => {
yText.insert(0, editorRef.getValue());
}, 0);
}
}
} else {
// Yjs content is not synchronized
}
};
yProvider.on("sync", onSync);
setProvider(yProvider);
const binding = new MonacoBinding(
yText,
model,
new Set([editorRef]),
yProvider.awareness as Awareness
);
return () => {
yDoc.destroy();
yProvider.destroy();
binding.destroy();
yProvider.off("sync", onSync);
};
}, [editorRef, room, activeFileContent]);
2024-05-03 13:53:21 -07:00
2024-05-02 17:38:37 -07:00
// connection/disconnection effect + resizeobserver
2024-04-26 22:34:56 -04:00
useEffect(() => {
socket.connect();
2024-04-26 22:34:56 -04:00
2024-05-02 17:38:37 -07:00
if (editorContainerRef.current) {
resizeObserver.observe(editorContainerRef.current);
2024-05-02 17:38:37 -07:00
}
2024-04-26 22:34:56 -04:00
return () => {
socket.disconnect();
2024-05-02 17:38:37 -07:00
resizeObserver.disconnect();
};
}, []);
2024-04-26 22:34:56 -04:00
// event listener effect
useEffect(() => {
const onConnect = () => {
console.log("connected");
2024-05-06 23:34:45 -07:00
createTerminal();
};
2024-04-28 20:06:47 -04:00
const onDisconnect = () => {};
2024-04-28 20:06:47 -04:00
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
setFiles(files);
};
2024-04-26 22:34:56 -04:00
2024-05-05 14:33:09 -07:00
const onRateLimit = (message: string) => {
toast.error(message);
};
2024-04-28 20:06:47 -04:00
const onTerminalResponse = (response: { id: string; data: string }) => {
const res = response.data;
console.log("terminal response:", res);
const term = terminals.find((t) => t.id === response.id);
if (term && term.terminal) term.terminal.write(res);
};
socket.on("connect", onConnect);
socket.on("disconnect", onDisconnect);
socket.on("loaded", onLoadedEvent);
socket.on("rateLimit", onRateLimit);
socket.on("terminalResponse", onTerminalResponse);
2024-04-26 22:34:56 -04:00
return () => {
socket.off("connect", onConnect);
socket.off("disconnect", onDisconnect);
socket.off("loaded", onLoadedEvent);
socket.off("rateLimit", onRateLimit);
socket.off("terminalResponse", onTerminalResponse);
};
}, []);
2024-04-26 22:34:56 -04:00
2024-04-28 20:06:47 -04:00
// Helper functions:
2024-04-06 19:03:04 -04:00
const createTerminal = () => {
2024-05-06 23:34:45 -07:00
setCreatingTerminal(true);
const id = createId();
setActiveTerminalId(id);
setTimeout(() => {
socket.emit("createTerminal", id, (res: boolean) => {
if (res) {
setTerminals((prev) => [...prev, { id, terminal: null }]);
}
});
}, 1000);
setCreatingTerminal(false);
};
2024-04-27 11:08:19 -04:00
const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return;
const exists = tabs.find((t) => t.id === tab.id);
2024-05-05 12:55:34 -07:00
2024-04-26 00:10:53 -04:00
setTabs((prev) => {
if (exists) {
setActiveFileId(exists.id);
return prev;
2024-04-26 00:10:53 -04:00
}
return [...prev, tab];
});
2024-05-04 01:18:06 -07:00
2024-04-27 00:20:17 -04:00
socket.emit("getFile", tab.id, (response: string) => {
setActiveFileContent(response);
});
setEditorLanguage(processFileType(tab.name));
setActiveFileId(tab.id);
};
2024-04-26 00:10:53 -04:00
const closeTab = (tab: TFile) => {
const numTabs = tabs.length;
const index = tabs.findIndex((t) => t.id === tab.id);
2024-04-30 01:56:43 -04:00
if (index === -1) return;
2024-04-30 01:56:43 -04:00
2024-04-27 00:20:17 -04:00
const nextId =
activeFileId === tab.id
2024-04-27 00:20:17 -04:00
? numTabs === 1
? null
: index < numTabs - 1
? tabs[index + 1].id
: tabs[index - 1].id
: activeFileId;
2024-04-27 00:20:17 -04:00
setTabs((prev) => prev.filter((t) => t.id !== tab.id));
2024-05-04 01:18:06 -07:00
if (!nextId) {
setActiveFileId("");
2024-05-04 01:18:06 -07:00
} else {
const nextTab = tabs.find((t) => t.id === nextId);
2024-05-04 01:18:06 -07:00
if (nextTab) {
selectFile(nextTab);
2024-05-04 01:18:06 -07:00
}
}
};
2024-04-26 00:10:53 -04:00
2024-05-06 23:34:45 -07:00
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;
socket.emit("closeTerminal", term.id, (res: boolean) => {
if (res) {
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);
}
}
}
});
};
2024-04-27 14:23:09 -04:00
const handleRename = (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => {
const valid = validateName(newName, oldName, type);
2024-05-05 12:55:34 -07:00
if (!valid.status) {
if (valid.message) toast.error("Invalid file name.");
return false;
2024-04-30 01:56:43 -04:00
}
2024-04-27 14:23:09 -04:00
socket.emit("renameFile", id, newName);
2024-04-27 11:08:19 -04:00
setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
);
2024-04-27 14:23:09 -04:00
return true;
};
2024-04-27 11:08:19 -04:00
2024-04-30 01:56:43 -04:00
const handleDeleteFile = (file: TFile) => {
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response);
});
closeTab(file);
};
2024-04-30 01:56:43 -04:00
const handleDeleteFolder = (folder: TFolder) => {
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
// setFiles(response)
// })
};
2024-04-30 01:56:43 -04:00
2024-04-06 19:03:04 -04:00
return (
<>
2024-05-02 17:38:37 -07:00
<div ref={generateRef} />
<div className="z-50 p-1" ref={generateWidgetRef}>
2024-05-04 01:50:33 -07:00
{generate.show && ai ? (
2024-05-02 17:38:37 -07:00
<GenerateInput
user={userData}
2024-05-03 00:52:01 -07:00
socket={socket}
2024-05-02 17:38:37 -07:00
width={generate.width - 90}
2024-05-03 00:52:01 -07:00
data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "",
2024-05-03 00:52:01 -07:00
line: generate.line,
}}
editor={{
language: editorLanguage,
}}
2024-05-02 17:38:37 -07:00
onExpand={() => {
editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id);
2024-05-02 17:38:37 -07:00
if (!generateRef.current) return;
2024-05-02 17:38:37 -07:00
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
});
2024-05-02 17:38:37 -07:00
setGenerate((prev) => {
return { ...prev, id };
});
});
2024-05-02 17:38:37 -07:00
}}
2024-05-02 18:05:18 -07:00
onAccept={(code: string) => {
const line = generate.line;
2024-05-02 18:05:18 -07:00
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
};
});
const file = editorRef?.getValue();
const lines = file?.split("\n") || [];
lines.splice(line - 1, 0, code);
const updatedFile = lines.join("\n");
editorRef?.setValue(updatedFile);
2024-05-02 18:05:18 -07:00
}}
2024-05-02 17:38:37 -07:00
/>
) : null}
2024-05-02 00:00:35 -07:00
</div>
2024-05-02 17:38:37 -07:00
2024-04-27 14:23:09 -04:00
<Sidebar
files={files}
selectFile={selectFile}
handleRename={handleRename}
2024-04-30 01:56:43 -04:00
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
2024-04-29 00:50:25 -04:00
socket={socket}
addNew={(name, type) => {
if (type === "file") {
setFiles((prev) => [
...prev,
2024-05-05 00:06:10 -07:00
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
]);
2024-04-29 00:50:25 -04:00
} else {
console.log("adding folder");
2024-04-29 00:50:25 -04:00
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
}
}}
2024-05-04 01:50:33 -07:00
ai={ai}
setAi={setAi}
2024-04-27 14:23:09 -04:00
/>
2024-04-06 19:03:04 -04:00
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
className="p-2 flex flex-col"
maxSize={75}
minSize={30}
defaultSize={60}
>
<div className="h-10 w-full flex gap-2">
2024-04-26 00:10:53 -04:00
{tabs.map((tab) => (
<Tab
key={tab.id}
2024-04-27 11:08:19 -04:00
saved={tab.saved}
selected={activeFileId === tab.id}
2024-05-04 01:18:06 -07:00
onClick={(e) => {
selectFile(tab);
2024-04-27 00:20:17 -04:00
}}
2024-04-26 00:10:53 -04:00
onClose={() => closeTab(tab)}
>
{tab.name}
</Tab>
))}
2024-04-06 19:03:04 -04:00
</div>
2024-05-02 17:38:37 -07:00
<div
ref={editorContainerRef}
2024-05-03 14:27:45 -07:00
className="grow w-full overflow-hidden rounded-md relative"
2024-05-02 17:38:37 -07:00
>
{!activeFileId ? (
2024-04-26 00:10:53 -04:00
<>
2024-05-05 14:33:09 -07:00
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
2024-04-27 11:08:19 -04:00
<FileJson className="w-6 h-6 mr-3" />
2024-04-26 00:10:53 -04:00
No file selected.
</div>
</>
) : clerk.loaded ? (
2024-05-03 14:27:45 -07:00
<>
{provider ? <Cursors yProvider={provider} /> : 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
)
);
}
2024-05-03 14:27:45 -07:00
}}
options={{
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
theme="vs-dark"
2024-05-06 23:34:45 -07:00
value={activeFileContent}
2024-05-03 14:27:45 -07:00
/>
</>
2024-05-05 14:33:09 -07:00
) : (
<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>
)}
2024-04-06 19:03:04 -04:00
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
2024-04-08 23:40:42 -04:00
<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">
2024-04-09 00:50:48 -04:00
<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">
2024-04-08 23:40:42 -04:00
<TerminalSquare className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<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">
2024-04-08 23:40:42 -04:00
<ChevronLeft className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<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">
2024-04-08 23:40:42 -04:00
<ChevronRight className="w-4 h-4" />
</div>
2024-04-09 00:50:48 -04:00
<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">
2024-04-08 23:40:42 -04:00
<RotateCw className="w-3 h-3" />
</div>
</div>
</div>
2024-04-06 19:03:04 -04:00
</div>
2024-04-08 23:40:42 -04:00
<div className="w-full grow rounded-md bg-foreground"></div>
2024-04-06 19:03:04 -04:00
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
2024-05-05 14:33:09 -07:00
{isOwner ? (
<>
<div className="h-10 w-full flex gap-2 shrink-0">
2024-05-06 23:34:45 -07:00
{terminals.map((term) => (
<Tab
key={term.id}
onClick={() => setActiveTerminalId(term.id)}
onClose={() => closeTerminal(term)}
selected={activeTerminalId === term.id}
>
<SquareTerminal className="w-4 h-4 mr-2" />
Shell
</Tab>
))}
2024-05-05 14:33:09 -07:00
<Button
2024-05-06 23:34:45 -07:00
disabled={creatingTerminal}
2024-05-05 14:33:09 -07:00
onClick={() => {
if (terminals.length >= 4) {
toast.error(
"You reached the maximum # of terminals."
);
2024-05-06 23:34:45 -07:00
return;
2024-05-05 14:33:09 -07:00
}
2024-05-06 23:34:45 -07:00
createTerminal();
2024-05-05 14:33:09 -07:00
}}
size="smIcon"
variant={"secondary"}
className={`font-normal select-none text-muted-foreground`}
>
2024-05-06 23:34:45 -07:00
{creatingTerminal ? (
<Loader2 className="animate-spin w-4 h-4" />
) : (
<Plus className="w-4 h-4" />
)}
2024-05-05 14:33:09 -07:00
</Button>
</div>
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
2024-05-06 23:34:45 -07:00
{socket && activeTerminal ? (
<EditorTerminal
socket={socket}
id={activeTerminal.id}
term={activeTerminal.terminal}
setTerm={(t: Terminal) => {
setTerminals((prev) =>
prev.map((term) =>
term.id === activeTerminal.id
? { ...term, terminal: t }
: term
)
);
}}
/>
) : null}
2024-05-05 14:33:09 -07:00
</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 terminal access.
</div>
)}
2024-04-06 19:03:04 -04:00
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</>
);
2024-04-06 19:03:04 -04:00
}