add resizing logic
This commit is contained in:
parent
1500e84724
commit
e86e86dbe2
@ -263,8 +263,15 @@ io.on("connection", async (socket) => {
|
|||||||
callback()
|
callback()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => {
|
||||||
|
console.log("resizeTerminal", dimensions)
|
||||||
|
Object.values(terminals).forEach((t) => {
|
||||||
|
t.terminal.resize(dimensions.cols, dimensions.rows)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
socket.on("terminalData", (id: string, data: string) => {
|
socket.on("terminalData", (id: string, data: string) => {
|
||||||
console.log("terminalData", id, data)
|
|
||||||
if (!terminals[id]) {
|
if (!terminals[id]) {
|
||||||
console.log("terminal not found", id)
|
console.log("terminal not found", id)
|
||||||
return
|
return
|
||||||
@ -282,13 +289,10 @@ io.on("connection", async (socket) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("closing terminal", id)
|
|
||||||
terminals[id].onData.dispose()
|
terminals[id].onData.dispose()
|
||||||
terminals[id].onExit.dispose()
|
terminals[id].onExit.dispose()
|
||||||
delete terminals[id]
|
delete terminals[id]
|
||||||
|
|
||||||
console.log("terminals:", Object.keys(terminals))
|
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -330,7 +334,7 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
if (data.isOwner) {
|
if (data.isOwner) {
|
||||||
console.log("deleting all terminals")
|
// console.log("deleting all terminals")
|
||||||
Object.entries(terminals).forEach((t) => {
|
Object.entries(terminals).forEach((t) => {
|
||||||
const { terminal, onData, onExit } = t[1]
|
const { terminal, onData, onExit } = t[1]
|
||||||
onData.dispose()
|
onData.dispose()
|
||||||
|
@ -120,7 +120,12 @@ export default function NewProjectModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open: boolean) => {
|
||||||
|
if (!loading) setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create A Sandbox</DialogTitle>
|
<DialogTitle>Create A Sandbox</DialogTitle>
|
||||||
|
@ -9,6 +9,7 @@ import Link from "next/link";
|
|||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function DashboardProjects({
|
export default function DashboardProjects({
|
||||||
sandboxes,
|
sandboxes,
|
||||||
@ -17,9 +18,16 @@ export default function DashboardProjects({
|
|||||||
sandboxes: Sandbox[];
|
sandboxes: Sandbox[];
|
||||||
q: string | null;
|
q: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const [deletingId, setDeletingId] = useState<string>("");
|
||||||
|
|
||||||
const onDelete = async (sandbox: Sandbox) => {
|
const onDelete = async (sandbox: Sandbox) => {
|
||||||
|
setDeletingId(sandbox.id);
|
||||||
toast(`Project ${sandbox.name} deleted.`);
|
toast(`Project ${sandbox.name} deleted.`);
|
||||||
await deleteSandbox(sandbox.id);
|
await deleteSandbox(sandbox.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
// timeout to wait for revalidatePath and avoid flashing
|
||||||
|
setDeletingId("");
|
||||||
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onVisibilityChange = async (sandbox: Sandbox) => {
|
const onVisibilityChange = async (sandbox: Sandbox) => {
|
||||||
@ -50,7 +58,11 @@ export default function DashboardProjects({
|
|||||||
<Link
|
<Link
|
||||||
key={sandbox.id}
|
key={sandbox.id}
|
||||||
href={`/code/${sandbox.id}`}
|
href={`/code/${sandbox.id}`}
|
||||||
className="cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg"
|
className={`${
|
||||||
|
deletingId === sandbox.id
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: ""
|
||||||
|
} cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
|
||||||
>
|
>
|
||||||
<Card className="p-4 h-48 flex flex-col justify-between items-start hover:border-foreground transition-all">
|
<Card className="p-4 h-48 flex flex-col justify-between items-start hover:border-foreground transition-all">
|
||||||
{/* <ProjectCard key={sandbox.id} id={sandbox.id}> */}
|
{/* <ProjectCard key={sandbox.id} id={sandbox.id}> */}
|
||||||
|
@ -30,6 +30,7 @@ import DisableAccessModal from "./live/disableModal";
|
|||||||
import Loading from "./loading";
|
import Loading from "./loading";
|
||||||
import PreviewWindow from "./preview";
|
import PreviewWindow from "./preview";
|
||||||
import Terminals from "./terminals";
|
import Terminals from "./terminals";
|
||||||
|
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
@ -44,12 +45,28 @@ export default function CodeEditor({
|
|||||||
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(
|
||||||
|
sandboxData.type !== "react"
|
||||||
|
);
|
||||||
|
const [disableAccess, setDisableAccess] = useState({
|
||||||
|
isDisabled: false,
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// File state
|
||||||
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 [activeFileId, setActiveFileId] = useState<string>("");
|
const [activeFileId, setActiveFileId] = useState<string>("");
|
||||||
const [activeFileContent, setActiveFileContent] = useState("");
|
const [activeFileContent, setActiveFileContent] = useState("");
|
||||||
|
|
||||||
|
// Editor state
|
||||||
|
const [editorLanguage, setEditorLanguage] = useState("plaintext");
|
||||||
const [cursorLine, setCursorLine] = useState(0);
|
const [cursorLine, setCursorLine] = useState(0);
|
||||||
|
const [editorRef, setEditorRef] =
|
||||||
|
useState<monaco.editor.IStandaloneCodeEditor>();
|
||||||
|
|
||||||
|
// AI Copilot state
|
||||||
|
const [ai, setAi] = useState(false);
|
||||||
const [generate, setGenerate] = useState<{
|
const [generate, setGenerate] = useState<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
@ -62,6 +79,8 @@ export default function CodeEditor({
|
|||||||
options: monaco.editor.IModelDeltaDecoration[];
|
options: monaco.editor.IModelDeltaDecoration[];
|
||||||
instance: monaco.editor.IEditorDecorationsCollection | undefined;
|
instance: monaco.editor.IEditorDecorationsCollection | undefined;
|
||||||
}>({ options: [], instance: undefined });
|
}>({ options: [], instance: undefined });
|
||||||
|
|
||||||
|
// Terminal state
|
||||||
const [terminals, setTerminals] = useState<
|
const [terminals, setTerminals] = useState<
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
@ -71,24 +90,21 @@ export default function CodeEditor({
|
|||||||
const [activeTerminalId, setActiveTerminalId] = useState("");
|
const [activeTerminalId, setActiveTerminalId] = useState("");
|
||||||
const [creatingTerminal, setCreatingTerminal] = useState(false);
|
const [creatingTerminal, setCreatingTerminal] = useState(false);
|
||||||
const [closingTerminal, setClosingTerminal] = useState("");
|
const [closingTerminal, setClosingTerminal] = useState("");
|
||||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
||||||
const [ai, setAi] = useState(false);
|
|
||||||
const [disableAccess, setDisableAccess] = useState({
|
|
||||||
isDisabled: false,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id;
|
const isOwner = sandboxData.userId === userData.id;
|
||||||
const clerk = useClerk();
|
const clerk = useClerk();
|
||||||
const room = useRoom();
|
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
|
||||||
|
|
||||||
const [editorRef, setEditorRef] =
|
// Liveblocks hooks
|
||||||
useState<monaco.editor.IStandaloneCodeEditor>();
|
const room = useRoom();
|
||||||
|
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
|
||||||
|
|
||||||
|
// Refs for libraries / features
|
||||||
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 previewPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
|
|
||||||
// Resize observer tracks editor width for generate widget
|
// Resize observer tracks editor width for generate widget
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
@ -371,6 +387,7 @@ export default function CodeEditor({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDisableAccess = (message: string) => {
|
const onDisableAccess = (message: string) => {
|
||||||
|
if (!isOwner)
|
||||||
setDisableAccess({
|
setDisableAccess({
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
message,
|
message,
|
||||||
@ -571,7 +588,7 @@ export default function CodeEditor({
|
|||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
maxSize={75}
|
maxSize={80}
|
||||||
minSize={30}
|
minSize={30}
|
||||||
defaultSize={60}
|
defaultSize={60}
|
||||||
>
|
>
|
||||||
@ -659,11 +676,22 @@ export default function CodeEditor({
|
|||||||
<ResizablePanel defaultSize={40}>
|
<ResizablePanel defaultSize={40}>
|
||||||
<ResizablePanelGroup direction="vertical">
|
<ResizablePanelGroup direction="vertical">
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
|
ref={previewPanelRef}
|
||||||
defaultSize={50}
|
defaultSize={50}
|
||||||
minSize={20}
|
collapsedSize={4}
|
||||||
|
minSize={25}
|
||||||
|
collapsible
|
||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
|
onCollapse={() => setIsPreviewCollapsed(true)}
|
||||||
|
onExpand={() => setIsPreviewCollapsed(false)}
|
||||||
>
|
>
|
||||||
<PreviewWindow />
|
<PreviewWindow
|
||||||
|
collapsed={isPreviewCollapsed}
|
||||||
|
open={() => {
|
||||||
|
previewPanelRef.current?.expand();
|
||||||
|
setIsPreviewCollapsed(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
|
@ -5,31 +5,69 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
|
UnfoldVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function PreviewWindow() {
|
export default function PreviewWindow({
|
||||||
|
collapsed,
|
||||||
|
open,
|
||||||
|
}: {
|
||||||
|
collapsed: boolean;
|
||||||
|
open: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-10 select-none w-full flex gap-2">
|
<div
|
||||||
|
className={`${
|
||||||
|
collapsed ? "h-full" : "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">
|
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
|
||||||
Preview
|
Preview
|
||||||
<div className="flex space-x-1 translate-x-1">
|
<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">
|
{collapsed ? (
|
||||||
|
<PreviewButton onClick={open}>
|
||||||
|
<UnfoldVertical className="w-4 h-4" />
|
||||||
|
</PreviewButton>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PreviewButton onClick={() => console.log("Terminal")}>
|
||||||
<TerminalSquare className="w-4 h-4" />
|
<TerminalSquare className="w-4 h-4" />
|
||||||
</div>
|
</PreviewButton>
|
||||||
<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">
|
<PreviewButton onClick={() => console.log("Back")}>
|
||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
</div>
|
</PreviewButton>
|
||||||
<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">
|
<PreviewButton onClick={() => console.log("Forward")}>
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</div>
|
</PreviewButton>
|
||||||
<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">
|
<PreviewButton onClick={() => console.log("Reload")}>
|
||||||
<RotateCw className="w-3 h-3" />
|
<RotateCw className="w-3 h-3" />
|
||||||
|
</PreviewButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{collapsed ? null : (
|
||||||
<div className="w-full grow rounded-md bg-foreground"></div>
|
<div className="w-full grow rounded-md bg-foreground"></div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PreviewButton({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -50,6 +50,7 @@ export default function Terminals({
|
|||||||
{terminals.map((term) => (
|
{terminals.map((term) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={term.id}
|
key={term.id}
|
||||||
|
creating={creatingTerminal}
|
||||||
onClick={() => setActiveTerminalId(term.id)}
|
onClick={() => setActiveTerminalId(term.id)}
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
closeTerminal({
|
closeTerminal({
|
||||||
@ -85,7 +86,7 @@ export default function Terminals({
|
|||||||
}}
|
}}
|
||||||
size="smIcon"
|
size="smIcon"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
className={`font-normal shrink-0 select-none text-muted-foreground`}
|
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
|
||||||
>
|
>
|
||||||
{creatingTerminal ? (
|
{creatingTerminal ? (
|
||||||
<Loader2 className="animate-spin w-4 h-4" />
|
<Loader2 className="animate-spin w-4 h-4" />
|
||||||
|
@ -48,19 +48,29 @@ export default function EditorTerminal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!term) return;
|
if (!term) return;
|
||||||
|
|
||||||
if (terminalRef.current) {
|
if (!terminalRef.current) return;
|
||||||
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();
|
||||||
}
|
|
||||||
const disposable = term.onData((data) => {
|
const disposableOnData = term.onData((data) => {
|
||||||
console.log("terminalData", id, data);
|
console.log("terminalData", id, data);
|
||||||
socket.emit("terminalData", id, data);
|
socket.emit("terminalData", id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const disposableOnResize = term.onResize((dimensions) => {
|
||||||
|
// const terminal_size = {
|
||||||
|
// width: dimensions.cols,
|
||||||
|
// height: dimensions.rows,
|
||||||
|
// };
|
||||||
|
fitAddon.fit();
|
||||||
|
socket.emit("terminalResize", dimensions);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposable.dispose();
|
disposableOnData.dispose();
|
||||||
|
disposableOnResize.dispose();
|
||||||
};
|
};
|
||||||
}, [term, terminalRef.current]);
|
}, [term, terminalRef.current]);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { MouseEvent, MouseEventHandler, useEffect } from "react";
|
|||||||
|
|
||||||
export default function Tab({
|
export default function Tab({
|
||||||
children,
|
children,
|
||||||
|
creating = false,
|
||||||
saved = true,
|
saved = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick,
|
onClick,
|
||||||
@ -13,6 +14,7 @@ export default function Tab({
|
|||||||
closing = false,
|
closing = false,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
creating?: boolean;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
@ -43,7 +45,7 @@ export default function Tab({
|
|||||||
}
|
}
|
||||||
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"
|
||||||
>
|
>
|
||||||
{closing ? (
|
{closing || creating ? (
|
||||||
<Loader2 className="animate-spin w-3 h-3" />
|
<Loader2 className="animate-spin w-3 h-3" />
|
||||||
) : saved ? (
|
) : saved ? (
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user