refactor terminal logic + state variables. temporarily break terminal
This commit is contained in:
parent
84c49f0d9d
commit
4e42555887
@ -223,6 +223,7 @@ io.on("connection", async (socket) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.on("createTerminal", ({ id }: { id: string }) => {
|
socket.on("createTerminal", ({ id }: { id: string }) => {
|
||||||
|
console.log("creating terminal", id)
|
||||||
if (terminals[id]) {
|
if (terminals[id]) {
|
||||||
console.log("Terminal already exists.")
|
console.log("Terminal already exists.")
|
||||||
return
|
return
|
||||||
@ -239,6 +240,7 @@ io.on("connection", async (socket) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onData = pty.onData((data) => {
|
const onData = pty.onData((data) => {
|
||||||
|
console.log("ondata")
|
||||||
socket.emit("terminalResponse", {
|
socket.emit("terminalResponse", {
|
||||||
// data: Buffer.from(data, "utf-8").toString("base64"),
|
// data: Buffer.from(data, "utf-8").toString("base64"),
|
||||||
data,
|
data,
|
||||||
@ -315,11 +317,11 @@ io.on("connection", async (socket) => {
|
|||||||
delete terminals[t[0]]
|
delete terminals[t[0]]
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("The owner disconnected.")
|
// console.log("The owner disconnected.")
|
||||||
socket.broadcast.emit("ownerDisconnected")
|
socket.broadcast.emit("ownerDisconnected")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("A shared user disconnected.")
|
// console.log("A shared user disconnected.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = await io.fetchSockets()
|
const sockets = await io.fetchSockets()
|
||||||
|
@ -1,64 +1,83 @@
|
|||||||
import Navbar from "@/components/editor/navbar"
|
import Navbar from "@/components/editor/navbar";
|
||||||
import { Room } from "@/components/editor/live/room"
|
import { Room } from "@/components/editor/live/room";
|
||||||
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
|
import { Sandbox, User, UsersToSandboxes } from "@/lib/types";
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs";
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic";
|
||||||
import { redirect } from "next/navigation"
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import Loading from "@/components/editor/loading";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
const CodeEditor = dynamic(() => import("@/components/editor"), {
|
const CodeEditor = dynamic(() => import("@/components/editor"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
const getUserData = async (id: string) => {
|
const getUserData = async (id: string) => {
|
||||||
const userRes = await fetch(
|
const userRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/user?id=${id}`
|
`https://database.ishaan1013.workers.dev/api/user?id=${id}`
|
||||||
)
|
);
|
||||||
const userData: User = await userRes.json()
|
const userData: User = await userRes.json();
|
||||||
return userData
|
return userData;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getSandboxData = async (id: string) => {
|
const getSandboxData = async (id: string) => {
|
||||||
const sandboxRes = await fetch(
|
const sandboxRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
|
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
|
||||||
)
|
);
|
||||||
const sandboxData: Sandbox = await sandboxRes.json()
|
const sandboxData: Sandbox = await sandboxRes.json();
|
||||||
return sandboxData
|
return sandboxData;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
|
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
|
||||||
const shared = await Promise.all(
|
const shared = await Promise.all(
|
||||||
usersToSandboxes.map(async (user) => {
|
usersToSandboxes.map(async (user) => {
|
||||||
const userRes = await fetch(
|
const userRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
|
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
|
||||||
)
|
);
|
||||||
const userData: User = await userRes.json()
|
const userData: User = await userRes.json();
|
||||||
return { id: userData.id, name: userData.name }
|
return { id: userData.id, name: userData.name };
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
return shared
|
return shared;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default async function CodePage({ params }: { params: { id: string } }) {
|
export default async function CodePage({ params }: { params: { id: string } }) {
|
||||||
const user = await currentUser()
|
const user = await currentUser();
|
||||||
const sandboxId = params.id
|
const sandboxId = params.id;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/")
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = await getUserData(user.id)
|
const userData = await getUserData(user.id);
|
||||||
const sandboxData = await getSandboxData(sandboxId)
|
const sandboxData = await getSandboxData(sandboxId);
|
||||||
const shared = await getSharedUsers(sandboxData.usersToSandboxes)
|
const shared = await getSharedUsers(sandboxData.usersToSandboxes);
|
||||||
|
|
||||||
|
const isOwner = sandboxData.userId === user.id;
|
||||||
|
const isSharedUser = shared.some((uts) => uts.id === user.id);
|
||||||
|
|
||||||
|
if (!isOwner && !isSharedUser) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||||
<Room id={sandboxId}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
<Room id={sandboxId}>
|
||||||
<div className="w-screen flex grow">
|
<Navbar
|
||||||
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
userData={userData}
|
||||||
</div>
|
sandboxData={sandboxData}
|
||||||
</Room>
|
shared={shared}
|
||||||
|
/>
|
||||||
|
<div className="w-screen flex grow">
|
||||||
|
<CodeEditor
|
||||||
|
userData={userData}
|
||||||
|
sandboxData={sandboxData}
|
||||||
|
isSharedUser={isSharedUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Room>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react";
|
||||||
import monaco from "monaco-editor"
|
import monaco from "monaco-editor";
|
||||||
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
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 * as Y from "yjs"
|
import * as Y from "yjs";
|
||||||
import LiveblocksProvider from "@liveblocks/yjs"
|
import LiveblocksProvider from "@liveblocks/yjs";
|
||||||
import { MonacoBinding } from "y-monaco"
|
import { MonacoBinding } from "y-monaco";
|
||||||
import { Awareness } from "y-protocols/awareness"
|
import { Awareness } from "y-protocols/awareness";
|
||||||
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
|
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable"
|
} from "@/components/ui/resizable";
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@ -28,56 +28,66 @@ import {
|
|||||||
Shell,
|
Shell,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
} from "lucide-react"
|
} 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 EditorTerminal from "./terminal";
|
||||||
import { Button } from "../ui/button"
|
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, TFileData, 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";
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
|
isSharedUser,
|
||||||
}: {
|
}: {
|
||||||
userData: User
|
userData: User;
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox;
|
||||||
|
isSharedUser: boolean;
|
||||||
}) {
|
}) {
|
||||||
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");
|
||||||
const [activeId, setActiveId] = useState<string>("")
|
const [activeFileId, setActiveFileId] = useState<string>("");
|
||||||
const [activeFile, setActiveFile] = useState<string | null>(null)
|
const [activeFileContent, setActiveFileContent] = useState<string | null>(
|
||||||
const [cursorLine, setCursorLine] = useState(0)
|
null
|
||||||
|
);
|
||||||
|
const [cursorLine, setCursorLine] = useState(0);
|
||||||
const [generate, setGenerate] = useState<{
|
const [generate, setGenerate] = useState<{
|
||||||
show: boolean
|
show: boolean;
|
||||||
id: string
|
id: string;
|
||||||
line: number
|
line: number;
|
||||||
widget: monaco.editor.IContentWidget | undefined
|
widget: monaco.editor.IContentWidget | undefined;
|
||||||
pref: monaco.editor.ContentWidgetPositionPreference[]
|
pref: monaco.editor.ContentWidgetPositionPreference[];
|
||||||
width: number
|
width: number;
|
||||||
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 })
|
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 });
|
||||||
const [decorations, setDecorations] = useState<{
|
const [decorations, setDecorations] = useState<{
|
||||||
options: monaco.editor.IModelDeltaDecoration[]
|
options: monaco.editor.IModelDeltaDecoration[];
|
||||||
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
instance: monaco.editor.IEditorDecorationsCollection | undefined;
|
||||||
}>({ options: [], instance: undefined })
|
}>({ options: [], instance: undefined });
|
||||||
const [terminals, setTerminals] = useState<string[]>([])
|
const [terminals, setTerminals] = useState<
|
||||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
{
|
||||||
const [ai, setAi] = useState(false)
|
id: string;
|
||||||
|
terminal: Terminal | null;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
|
||||||
|
const [ai, setAi] = useState(false);
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id
|
const isOwner = sandboxData.userId === userData.id;
|
||||||
const clerk = useClerk()
|
const clerk = useClerk();
|
||||||
const room = useRoom()
|
const room = useRoom();
|
||||||
|
|
||||||
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
|
// 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);
|
||||||
const monacoRef = useRef<typeof monaco | null>(null)
|
const monacoRef = useRef<typeof monaco | null>(null);
|
||||||
const generateRef = useRef<HTMLDivElement>(null)
|
const generateRef = useRef<HTMLDivElement>(null);
|
||||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
const generateWidgetRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleEditorWillMount: BeforeMount = (monaco) => {
|
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||||
monaco.editor.addKeybindingRules([
|
monaco.editor.addKeybindingRules([
|
||||||
@ -86,20 +96,20 @@ export default function CodeEditor({
|
|||||||
command: "null",
|
command: "null",
|
||||||
// when: "textInputFocus",
|
// when: "textInputFocus",
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleEditorMount: OnMount = (editor, monaco) => {
|
const handleEditorMount: OnMount = (editor, monaco) => {
|
||||||
setEditorRef(editor)
|
setEditorRef(editor);
|
||||||
monacoRef.current = monaco
|
monacoRef.current = monaco;
|
||||||
|
|
||||||
editor.onDidChangeCursorPosition((e) => {
|
editor.onDidChangeCursorPosition((e) => {
|
||||||
const { column, lineNumber } = e.position
|
const { column, lineNumber } = e.position;
|
||||||
if (lineNumber === cursorLine) return
|
if (lineNumber === cursorLine) return;
|
||||||
setCursorLine(lineNumber)
|
setCursorLine(lineNumber);
|
||||||
|
|
||||||
const model = editor.getModel()
|
const model = editor.getModel();
|
||||||
const endColumn = model?.getLineContent(lineNumber).length || 0
|
const endColumn = model?.getLineContent(lineNumber).length || 0;
|
||||||
|
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
@ -117,18 +127,18 @@ export default function CodeEditor({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
editor.onDidBlurEditorText((e) => {
|
editor.onDidBlurEditorText((e) => {
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
options: [],
|
options: [],
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
editor.addAction({
|
editor.addAction({
|
||||||
id: "generate",
|
id: "generate",
|
||||||
@ -142,11 +152,11 @@ export default function CodeEditor({
|
|||||||
...prev,
|
...prev,
|
||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
|
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ai) {
|
if (!ai) {
|
||||||
@ -154,32 +164,32 @@ export default function CodeEditor({
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: false,
|
show: false,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (generate.show) {
|
if (generate.show) {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
if (!generateRef.current) return
|
if (!generateRef.current) return;
|
||||||
const id = changeAccessor.addZone({
|
const id = changeAccessor.addZone({
|
||||||
afterLineNumber: cursorLine,
|
afterLineNumber: cursorLine,
|
||||||
heightInLines: 3,
|
heightInLines: 3,
|
||||||
domNode: generateRef.current,
|
domNode: generateRef.current,
|
||||||
})
|
});
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id, line: cursorLine }
|
return { ...prev, id, line: cursorLine };
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!generateWidgetRef.current) return
|
if (!generateWidgetRef.current) return;
|
||||||
const widgetElement = generateWidgetRef.current
|
const widgetElement = generateWidgetRef.current;
|
||||||
|
|
||||||
const contentWidget = {
|
const contentWidget = {
|
||||||
getDomNode: () => {
|
getDomNode: () => {
|
||||||
return widgetElement
|
return widgetElement;
|
||||||
},
|
},
|
||||||
getId: () => {
|
getId: () => {
|
||||||
return "generate.widget"
|
return "generate.widget";
|
||||||
},
|
},
|
||||||
getPosition: () => {
|
getPosition: () => {
|
||||||
return {
|
return {
|
||||||
@ -188,232 +198,253 @@ export default function CodeEditor({
|
|||||||
column: 1,
|
column: 1,
|
||||||
},
|
},
|
||||||
preference: generate.pref,
|
preference: generate.pref,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, widget: contentWidget }
|
return { ...prev, widget: contentWidget };
|
||||||
})
|
});
|
||||||
editorRef?.addContentWidget(contentWidget)
|
editorRef?.addContentWidget(contentWidget);
|
||||||
|
|
||||||
if (generateRef.current && generateWidgetRef.current) {
|
if (generateRef.current && generateWidgetRef.current) {
|
||||||
editorRef?.applyFontInfo(generateRef.current)
|
editorRef?.applyFontInfo(generateRef.current);
|
||||||
editorRef?.applyFontInfo(generateWidgetRef.current)
|
editorRef?.applyFontInfo(generateWidgetRef.current);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id)
|
changeAccessor.removeZone(generate.id);
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id: "" }
|
return { ...prev, id: "" };
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!generate.widget) return
|
if (!generate.widget) return;
|
||||||
editorRef?.removeContentWidget(generate.widget)
|
editorRef?.removeContentWidget(generate.widget);
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
widget: undefined,
|
widget: undefined,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [generate.show])
|
}, [generate.show]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (decorations.options.length === 0) {
|
if (decorations.options.length === 0) {
|
||||||
decorations.instance?.clear()
|
decorations.instance?.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ai) return
|
if (!ai) return;
|
||||||
|
|
||||||
if (decorations.instance) {
|
if (decorations.instance) {
|
||||||
decorations.instance.set(decorations.options)
|
decorations.instance.set(decorations.options);
|
||||||
} else {
|
} else {
|
||||||
const instance = editorRef?.createDecorationsCollection()
|
const instance = editorRef?.createDecorationsCollection();
|
||||||
instance?.set(decorations.options)
|
instance?.set(decorations.options);
|
||||||
|
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
instance,
|
instance,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [decorations.options])
|
}, [decorations.options]);
|
||||||
|
|
||||||
const socket = io(
|
const socket = io(
|
||||||
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
`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)) {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
|
|
||||||
// const activeTab = tabs.find((t) => t.id === activeId)
|
// const activeTab = tabs.find((t) => t.id === activeFileId)
|
||||||
// console.log("saving:", activeTab?.name, editorRef?.getValue())
|
// console.log("saving:", activeTab?.name, editorRef?.getValue())
|
||||||
|
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeId ? { ...tab, saved: true } : tab
|
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
socket.emit("saveFile", activeId, editorRef?.getValue())
|
socket.emit("saveFile", activeFileId, editorRef?.getValue());
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down);
|
||||||
}
|
};
|
||||||
}, [tabs, activeId])
|
}, [tabs, activeFileId]);
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const { width } = entry.contentRect
|
const { width } = entry.contentRect;
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, width }
|
return { ...prev, width };
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = tabs.find((t) => t.id === activeId)
|
const tab = tabs.find((t) => t.id === activeFileId);
|
||||||
const model = editorRef?.getModel()
|
const model = editorRef?.getModel();
|
||||||
|
|
||||||
if (!editorRef || !tab || !model) return
|
if (!editorRef || !tab || !model) return;
|
||||||
|
|
||||||
const yDoc = new Y.Doc()
|
const yDoc = new Y.Doc();
|
||||||
const yText = yDoc.getText(tab.id)
|
const yText = yDoc.getText(tab.id);
|
||||||
const yProvider: any = new LiveblocksProvider(room, yDoc)
|
const yProvider: any = new LiveblocksProvider(room, yDoc);
|
||||||
|
|
||||||
const onSync = (isSynced: boolean) => {
|
const onSync = (isSynced: boolean) => {
|
||||||
if (isSynced) {
|
if (isSynced) {
|
||||||
const text = yText.toString()
|
const text = yText.toString();
|
||||||
if (text === "") {
|
if (text === "") {
|
||||||
if (activeFile) {
|
if (activeFileContent) {
|
||||||
yText.insert(0, activeFile)
|
yText.insert(0, activeFileContent);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
yText.insert(0, editorRef.getValue())
|
yText.insert(0, editorRef.getValue());
|
||||||
}, 0)
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Yjs content is not synchronized
|
// Yjs content is not synchronized
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
yProvider.on("sync", onSync)
|
yProvider.on("sync", onSync);
|
||||||
|
|
||||||
setProvider(yProvider)
|
setProvider(yProvider);
|
||||||
|
|
||||||
const binding = new MonacoBinding(
|
const binding = new MonacoBinding(
|
||||||
yText,
|
yText,
|
||||||
model,
|
model,
|
||||||
new Set([editorRef]),
|
new Set([editorRef]),
|
||||||
yProvider.awareness as Awareness
|
yProvider.awareness as Awareness
|
||||||
)
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
yDoc.destroy()
|
yDoc.destroy();
|
||||||
yProvider.destroy()
|
yProvider.destroy();
|
||||||
binding.destroy()
|
binding.destroy();
|
||||||
yProvider.off("sync", onSync)
|
yProvider.off("sync", onSync);
|
||||||
}
|
};
|
||||||
}, [editorRef, room, activeFile])
|
}, [editorRef, room, activeFileContent]);
|
||||||
|
|
||||||
// connection/disconnection effect + resizeobserver
|
// connection/disconnection effect + resizeobserver
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect()
|
socket.connect();
|
||||||
|
|
||||||
if (editorContainerRef.current) {
|
if (editorContainerRef.current) {
|
||||||
resizeObserver.observe(editorContainerRef.current)
|
resizeObserver.observe(editorContainerRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.disconnect()
|
socket.disconnect();
|
||||||
|
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect();
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// event listener effect
|
// event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => {}
|
const onConnect = () => {
|
||||||
|
console.log("connected");
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.emit("createTerminal", { id: "testId" });
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const onDisconnect = () => {}
|
const onDisconnect = () => {};
|
||||||
|
|
||||||
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
|
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
|
||||||
setFiles(files)
|
setFiles(files);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onRateLimit = (message: string) => {
|
const onRateLimit = (message: string) => {
|
||||||
toast.error(message)
|
toast.error(message);
|
||||||
}
|
};
|
||||||
|
|
||||||
socket.on("connect", onConnect)
|
const onTerminalResponse = (response: { id: string; data: string }) => {
|
||||||
socket.on("disconnect", onDisconnect)
|
const res = response.data;
|
||||||
socket.on("loaded", onLoadedEvent)
|
console.log("terminal response:", res);
|
||||||
socket.on("rateLimit", onRateLimit)
|
|
||||||
|
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);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("connect", onConnect)
|
socket.off("connect", onConnect);
|
||||||
socket.off("disconnect", onDisconnect)
|
socket.off("disconnect", onDisconnect);
|
||||||
socket.off("loaded", onLoadedEvent)
|
socket.off("loaded", onLoadedEvent);
|
||||||
socket.off("rateLimit", onRateLimit)
|
socket.off("rateLimit", onRateLimit);
|
||||||
}
|
socket.off("terminalResponse", onTerminalResponse);
|
||||||
}, [])
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Helper functions:
|
// Helper functions:
|
||||||
|
|
||||||
|
const createTerminal = () => {
|
||||||
|
const id = "testId";
|
||||||
|
|
||||||
|
socket.emit("createTerminal", { id });
|
||||||
|
};
|
||||||
|
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
if (tab.id === activeId) return
|
if (tab.id === activeFileId) return;
|
||||||
const exists = tabs.find((t) => t.id === tab.id)
|
const exists = tabs.find((t) => t.id === tab.id);
|
||||||
|
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
setActiveId(exists.id)
|
setActiveFileId(exists.id);
|
||||||
return prev
|
return prev;
|
||||||
}
|
}
|
||||||
return [...prev, tab]
|
return [...prev, tab];
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.emit("getFile", tab.id, (response: string) => {
|
socket.emit("getFile", tab.id, (response: string) => {
|
||||||
setActiveFile(response)
|
setActiveFileContent(response);
|
||||||
})
|
});
|
||||||
setEditorLanguage(processFileType(tab.name))
|
setEditorLanguage(processFileType(tab.name));
|
||||||
setActiveId(tab.id)
|
setActiveFileId(tab.id);
|
||||||
}
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if (index === -1) return
|
if (index === -1) return;
|
||||||
|
|
||||||
const nextId =
|
const nextId =
|
||||||
activeId === tab.id
|
activeFileId === tab.id
|
||||||
? 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
|
||||||
: activeId
|
: activeFileId;
|
||||||
|
|
||||||
setTabs((prev) => prev.filter((t) => t.id !== tab.id))
|
setTabs((prev) => prev.filter((t) => t.id !== tab.id));
|
||||||
|
|
||||||
if (!nextId) {
|
if (!nextId) {
|
||||||
setActiveId("")
|
setActiveFileId("");
|
||||||
} else {
|
} else {
|
||||||
const nextTab = tabs.find((t) => t.id === nextId)
|
const nextTab = tabs.find((t) => t.id === nextId);
|
||||||
if (nextTab) {
|
if (nextTab) {
|
||||||
selectFile(nextTab)
|
selectFile(nextTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleRename = (
|
const handleRename = (
|
||||||
id: string,
|
id: string,
|
||||||
@ -421,32 +452,32 @@ export default function CodeEditor({
|
|||||||
oldName: string,
|
oldName: string,
|
||||||
type: "file" | "folder"
|
type: "file" | "folder"
|
||||||
) => {
|
) => {
|
||||||
const valid = validateName(newName, oldName, type)
|
const valid = validateName(newName, oldName, type);
|
||||||
if (!valid.status) {
|
if (!valid.status) {
|
||||||
if (valid.message) toast.error("Invalid file name.")
|
if (valid.message) toast.error("Invalid file name.");
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("renameFile", id, newName)
|
socket.emit("renameFile", id, newName);
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
||||||
)
|
);
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeleteFile = (file: TFile) => {
|
const handleDeleteFile = (file: TFile) => {
|
||||||
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response);
|
||||||
})
|
});
|
||||||
closeTab(file)
|
closeTab(file);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeleteFolder = (folder: TFolder) => {
|
const handleDeleteFolder = (folder: TFolder) => {
|
||||||
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
||||||
// setFiles(response)
|
// setFiles(response)
|
||||||
// })
|
// })
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -458,7 +489,7 @@ export default function CodeEditor({
|
|||||||
socket={socket}
|
socket={socket}
|
||||||
width={generate.width - 90}
|
width={generate.width - 90}
|
||||||
data={{
|
data={{
|
||||||
fileName: tabs.find((t) => t.id === activeId)?.name ?? "",
|
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||||
code: editorRef?.getValue() ?? "",
|
code: editorRef?.getValue() ?? "",
|
||||||
line: generate.line,
|
line: generate.line,
|
||||||
}}
|
}}
|
||||||
@ -467,33 +498,33 @@ export default function CodeEditor({
|
|||||||
}}
|
}}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id)
|
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) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id }
|
return { ...prev, id };
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
onAccept={(code: string) => {
|
onAccept={(code: string) => {
|
||||||
const line = generate.line
|
const line = generate.line;
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
const file = editorRef?.getValue()
|
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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -511,9 +542,9 @@ export default function CodeEditor({
|
|||||||
setFiles((prev) => [
|
setFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
|
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
|
||||||
])
|
]);
|
||||||
} else {
|
} else {
|
||||||
console.log("adding folder")
|
console.log("adding folder");
|
||||||
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
|
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -532,9 +563,9 @@ export default function CodeEditor({
|
|||||||
<Tab
|
<Tab
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
saved={tab.saved}
|
saved={tab.saved}
|
||||||
selected={activeId === tab.id}
|
selected={activeFileId === tab.id}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
selectFile(tab)
|
selectFile(tab);
|
||||||
}}
|
}}
|
||||||
onClose={() => closeTab(tab)}
|
onClose={() => closeTab(tab)}
|
||||||
>
|
>
|
||||||
@ -546,7 +577,7 @@ export default function CodeEditor({
|
|||||||
ref={editorContainerRef}
|
ref={editorContainerRef}
|
||||||
className="grow w-full overflow-hidden rounded-md relative"
|
className="grow w-full overflow-hidden rounded-md relative"
|
||||||
>
|
>
|
||||||
{!activeId ? (
|
{!activeFileId ? (
|
||||||
<>
|
<>
|
||||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
<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" />
|
<FileJson className="w-6 h-6 mr-3" />
|
||||||
@ -562,18 +593,22 @@ export default function CodeEditor({
|
|||||||
beforeMount={handleEditorWillMount}
|
beforeMount={handleEditorWillMount}
|
||||||
onMount={handleEditorMount}
|
onMount={handleEditorMount}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (value === activeFile) {
|
if (value === activeFileContent) {
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeId ? { ...tab, saved: true } : tab
|
tab.id === activeFileId
|
||||||
|
? { ...tab, saved: true }
|
||||||
|
: tab
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeId ? { ...tab, saved: false } : tab
|
tab.id === activeFileId
|
||||||
|
? { ...tab, saved: false }
|
||||||
|
: tab
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
@ -589,7 +624,7 @@ export default function CodeEditor({
|
|||||||
fontFamily: "var(--font-geist-mono)",
|
fontFamily: "var(--font-geist-mono)",
|
||||||
}}
|
}}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
value={activeFile ?? ""}
|
value={activeFileContent ?? ""}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -645,7 +680,9 @@ export default function CodeEditor({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (terminals.length >= 4) {
|
if (terminals.length >= 4) {
|
||||||
toast.error("You reached the maximum # of terminals.")
|
toast.error(
|
||||||
|
"You reached the maximum # of terminals."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size="smIcon"
|
size="smIcon"
|
||||||
@ -656,7 +693,9 @@ export default function CodeEditor({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
||||||
{socket ? <EditorTerminal socket={socket} /> : null}
|
{/* {socket ? <EditorTerminal socket={socket} term={
|
||||||
|
|
||||||
|
} /> : null} */}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -670,5 +709,5 @@ export default function CodeEditor({
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Image from "next/image"
|
import Image from "next/image";
|
||||||
import Logo from "@/assets/logo.svg"
|
import Logo from "@/assets/logo.svg";
|
||||||
import { Skeleton } from "../ui/skeleton"
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import { Loader, Loader2 } from "lucide-react"
|
import { Loader, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
@ -31,11 +31,15 @@ export default function Loading() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-secondary select-none">
|
<div className="w-full h-full grid grid-cols-5 grid-rows-2 gap-4 p-2">
|
||||||
<Loader2 className="w-6 h-6 mr-3 animate-spin" />
|
<div className="w-full h-full col-span-3 row-span-2 flex items-center justify-center text-xl font-medium text-secondary select-none">
|
||||||
Loading...
|
<Loader2 className="w-6 h-6 mr-3 animate-spin" />
|
||||||
</div>{" "}
|
Loading...
|
||||||
|
</div>
|
||||||
|
<Skeleton className="w-full h-full col-span-2 rounded-md" />
|
||||||
|
<Skeleton className="w-full h-full col-span-2 rounded-md" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import { Sandbox } from "@/lib/types"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1).max(16),
|
name: z.string().min(1).max(16),
|
||||||
@ -69,6 +70,8 @@ export default function EditSandboxModal({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
await updateSandbox({ id: data.id, ...values })
|
await updateSandbox({ id: data.id, ...values })
|
||||||
|
|
||||||
|
toast.success("Sandbox updated successfully")
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Loader2, UserPlus, X } from "lucide-react"
|
import { Link, Loader2, UserPlus, X } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Sandbox } from "@/lib/types"
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@ -75,41 +75,49 @@ export default function ShareSandboxModal({
|
|||||||
{data.visibility === "private" ? (
|
{data.visibility === "private" ? (
|
||||||
<DialogDescription className="text-sm text-muted-foreground">
|
<DialogDescription className="text-sm text-muted-foreground">
|
||||||
This sandbox is private. Making it public will allow shared
|
This sandbox is private. Making it public will allow shared
|
||||||
users to view and collaborate.
|
users to view and collaborate. You can still share & manage access below.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<div className="flex space-x-4 w-full">
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex">
|
<Form {...form}>
|
||||||
<FormField
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex w-full">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="email"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="email"
|
||||||
<FormItem className="mr-4 w-full">
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem className="mr-4 w-full">
|
||||||
<Input
|
<FormControl>
|
||||||
placeholder="yourfriend@domain.com"
|
<Input
|
||||||
{...field}
|
placeholder="yourfriend@domain.com"
|
||||||
className="w-full"
|
{...field}
|
||||||
/>
|
className="w-full"
|
||||||
</FormControl>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<Button disabled={loading} type="submit" className="">
|
/>
|
||||||
{loading ? (
|
<Button disabled={loading} type="submit" className="">
|
||||||
<>
|
{loading ? (
|
||||||
<Loader2 className="animate-spin mr-2 h-4 w-4" /> Loading...
|
<>
|
||||||
</>
|
<Loader2 className="animate-spin mr-2 h-4 w-4" /> Loading...
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<UserPlus className="mr-2 h-4 w-4" /> Share
|
<>
|
||||||
</>
|
<UserPlus className="mr-2 h-4 w-4" /> Share
|
||||||
)}
|
</>
|
||||||
</Button>
|
)}
|
||||||
</form>
|
</Button>
|
||||||
</Form>
|
</form>
|
||||||
|
</Form>
|
||||||
|
<Button onClick={() => {
|
||||||
|
navigator.clipboard.writeText(`https://s.ishaand.com/code/${data.id}`)
|
||||||
|
toast.success("Link copied to clipboard.")
|
||||||
|
}} size="icon" disabled={loading} variant="outline" className="shrink-0">
|
||||||
|
<Link className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shared.length > 0 ? (
|
{shared.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Terminal } from "@xterm/xterm"
|
import { Terminal } from "@xterm/xterm";
|
||||||
import { FitAddon } from "@xterm/addon-fit"
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import "./xterm.css"
|
import "./xterm.css";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client";
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function EditorTerminal({ socket }: { socket: Socket }) {
|
export default function EditorTerminal({
|
||||||
const terminalRef = useRef(null)
|
socket,
|
||||||
const [term, setTerm] = useState<Terminal | null>(null)
|
term,
|
||||||
|
setTerm,
|
||||||
|
}: {
|
||||||
|
socket: Socket;
|
||||||
|
term: Terminal | null;
|
||||||
|
setTerm: (term: Terminal) => void;
|
||||||
|
}) {
|
||||||
|
const terminalRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminalRef.current) return
|
if (!terminalRef.current) return;
|
||||||
|
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
@ -22,55 +29,43 @@ export default function EditorTerminal({ socket }: { socket: Socket }) {
|
|||||||
},
|
},
|
||||||
fontFamily: "var(--font-geist-mono)",
|
fontFamily: "var(--font-geist-mono)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
})
|
});
|
||||||
|
|
||||||
setTerm(terminal)
|
setTerm(terminal);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (terminal) terminal.dispose()
|
if (terminal) terminal.dispose();
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!term) return
|
if (!term) return;
|
||||||
|
|
||||||
const onConnect = () => {
|
// const onTerminalResponse = (response: { data: string }) => {
|
||||||
console.log("Connected to server", socket.connected)
|
// const res = response.data;
|
||||||
setTimeout(() => {
|
// term.write(res);
|
||||||
socket.emit("createTerminal", { id: "testId" })
|
// };
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTerminalResponse = (response: { data: string }) => {
|
|
||||||
// const res = Buffer.from(response.data, "base64").toString("utf-8")
|
|
||||||
const res = response.data
|
|
||||||
term.write(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on("connect", onConnect)
|
|
||||||
|
|
||||||
if (terminalRef.current) {
|
if (terminalRef.current) {
|
||||||
socket.on("terminalResponse", onTerminalResponse)
|
// socket.on("terminalResponse", onTerminalResponse);
|
||||||
|
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon();
|
||||||
term.loadAddon(fitAddon)
|
term.loadAddon(fitAddon);
|
||||||
term.open(terminalRef.current)
|
term.open(terminalRef.current);
|
||||||
fitAddon.fit()
|
fitAddon.fit();
|
||||||
setTerm(term)
|
setTerm(term);
|
||||||
}
|
}
|
||||||
const disposable = term.onData((data) => {
|
const disposable = term.onData((data) => {
|
||||||
console.log("sending data", data)
|
console.log("sending data", data);
|
||||||
socket.emit("terminalData", "testId", data)
|
socket.emit("terminalData", "testId", data);
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.emit("terminalData", "\n")
|
// socket.emit("terminalData", "\n");
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("connect", onConnect)
|
disposable.dispose();
|
||||||
socket.off("terminalResponse", onTerminalResponse)
|
};
|
||||||
disposable.dispose()
|
}, [term, terminalRef.current]);
|
||||||
}
|
|
||||||
}, [term, terminalRef.current])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={terminalRef} className="w-full h-full text-left">
|
<div ref={terminalRef} className="w-full h-full text-left">
|
||||||
@ -81,5 +76,5 @@ export default function EditorTerminal({ socket }: { socket: Socket }) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user