Compare commits

..

1 Commits

Author SHA1 Message Date
af45b6196c chore: start to dev 2024-08-28 18:40:24 -04:00
8 changed files with 111 additions and 146 deletions

View File

@ -6,7 +6,6 @@ import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic"
import fs from "fs"
import { TerminalProvider } from "@/context/TerminalContext"
export const revalidate = 0
@ -88,10 +87,8 @@ export default async function CodePage({ params }: { params: { id: string } }) {
}
return (
<>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}>
<TerminalProvider>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow">
<CodeEditor
@ -99,9 +96,7 @@ export default async function CodePage({ params }: { params: { id: string } }) {
sandboxData={sandboxData}
/>
</div>
</TerminalProvider>
</Room>
</div>
</>
)
}

View File

@ -6,8 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react"
import { PreviewProvider } from "@/context/PreviewContext";
import { SocketProvider } from '@/context/SocketContext'
import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext"
export const metadata: Metadata = {
title: "Sandbox",
@ -15,7 +15,7 @@ export const metadata: Metadata = {
}
export default function RootLayout({
children
children,
}: Readonly<{
children: React.ReactNode
}>) {
@ -29,11 +29,11 @@ export default function RootLayout({
forcedTheme="dark"
disableTransitionOnChange
>
<SocketProvider>
<PreviewProvider>
<TerminalProvider>
{children}
</TerminalProvider>
</PreviewProvider>
</SocketProvider>
<Analytics />
<Toaster position="bottom-left" richColors />
</ThemeProvider>

View File

@ -49,9 +49,11 @@ export default function Dashboard({
const q = searchParams.get("q")
const router = useRouter()
useEffect(() => { // update the dashboard to show a new project
useEffect(() => {
if (!sandboxes) {
router.refresh()
}, [])
}
}, [sandboxes])
return (
<>

View File

@ -1,48 +1,38 @@
"use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
// Authentication
import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { Socket, io } from "socket.io-client"
import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs"
// Liveblocks
import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
// Icons
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
// Contexts
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useSocket } from "@/context/SocketContext"
// External Components
import monaco from "monaco-editor"
import { Terminal } from "@xterm/xterm"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { ImperativePanelHandle } from "react-resizable-panels"
// Editor Components
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
import Tab from "../ui/tab"
import Sidebar from "./sidebar"
import GenerateInput from "./generate"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
import { Cursors } from "./live/cursors"
import { Terminal } from "@xterm/xterm"
import DisableAccessModal from "./live/disableModal"
import Loading from "./loading"
import PreviewWindow from "./preview"
import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useTerminal } from '@/context/TerminalContext';
export default function CodeEditor({
userData,
@ -51,18 +41,27 @@ export default function CodeEditor({
userData: User
sandboxData: Sandbox
}) {
const socketRef = useRef<Socket | null>(null);
// Initialize socket connection if it doesn't exist
if (!socketRef.current) {
socketRef.current = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{
timeout: 2000,
}
);
}
//Terminalcontext functionsand effects
const { setUserAndSandboxId } = useTerminal();
// Socket to the backend server
const { socket, setUserAndSandboxId } = useSocket();
useEffect(() => {
// Pass the user and sandbox ID to the socket, causing the socket to be created and to connect.
setUserAndSandboxId(userData.id, sandboxData.id);
}, []);
}, [userData.id, sandboxData.id, setUserAndSandboxId]);
// Preview panel state
//Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
// When the owner closes the project, isDisabled gets set for the other users.
const [disableAccess, setDisableAccess] = useState({
isDisabled: false,
message: "",
@ -334,9 +333,9 @@ export default function CodeEditor({
);
console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`);
socket?.emit("saveFile", activeFileId, value);
socketRef.current?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socket]
[socketRef]
);
useEffect(() => {
@ -433,11 +432,11 @@ export default function CodeEditor({
// Connection/disconnection effect
useEffect(() => {
socket?.connect()
socketRef.current?.connect()
return () => {
socket?.disconnect()
socketRef.current?.disconnect()
}
}, [socket])
}, [])
// Socket event listener effect
useEffect(() => {
@ -470,24 +469,25 @@ export default function CodeEditor({
})
}
socket?.on("connect", onConnect)
socket?.on("disconnect", onDisconnect)
socket?.on("loaded", onLoadedEvent)
socket?.on("error", onError)
socket?.on("terminalResponse", onTerminalResponse)
socket?.on("disableAccess", onDisableAccess)
socket?.on("previewURL", loadPreviewURL)
socketRef.current?.on("connect", onConnect)
socketRef.current?.on("disconnect", onDisconnect)
socketRef.current?.on("loaded", onLoadedEvent)
socketRef.current?.on("error", onError)
socketRef.current?.on("terminalResponse", onTerminalResponse)
socketRef.current?.on("disableAccess", onDisableAccess)
socketRef.current?.on("previewURL", loadPreviewURL)
return () => {
socket?.off("connect", onConnect)
socket?.off("disconnect", onDisconnect)
socket?.off("loaded", onLoadedEvent)
socket?.off("error", onError)
socket?.off("terminalResponse", onTerminalResponse)
socket?.off("disableAccess", onDisableAccess)
socket?.off("previewURL", loadPreviewURL)
socketRef.current?.off("connect", onConnect)
socketRef.current?.off("disconnect", onDisconnect)
socketRef.current?.off("loaded", onLoadedEvent)
socketRef.current?.off("error", onError)
socketRef.current?.off("terminalResponse", onTerminalResponse)
socketRef.current?.off("disableAccess", onDisableAccess)
socketRef.current?.off("previewURL", loadPreviewURL)
}
}, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
// }, []);
}, [terminals])
// Helper functions for tabs:
@ -497,13 +497,14 @@ export default function CodeEditor({
const fileCache = useRef(new Map());
// Debounced function to get file content
const debouncedGetFile =
(tabId: any, callback: any) => {
socket?.emit('getFile', tabId, callback);
} // 300ms debounce delay, adjust as needed
const selectFile = (tab: TTab) => {
const debouncedGetFile = useCallback(
debounce((tabId, callback) => {
socketRef.current?.emit('getFile', tabId, callback);
}, 300), // 300ms debounce delay, adjust as needed
[]
);
const selectFile = useCallback((tab: TTab) => {
if (tab.id === activeFileId) return;
setGenerate((prev) => ({ ...prev, show: false }));
@ -528,7 +529,7 @@ export default function CodeEditor({
setEditorLanguage(processFileType(tab.name));
setActiveFileId(tab.id);
};
}, [activeFileId, tabs, debouncedGetFile]);
// Close tab and remove from tabs
const closeTab = (id: string) => {
@ -602,7 +603,7 @@ export default function CodeEditor({
return false
}
socket?.emit("renameFile", id, newName)
socketRef.current?.emit("renameFile", id, newName)
setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
)
@ -611,7 +612,7 @@ export default function CodeEditor({
}
const handleDeleteFile = (file: TFile) => {
socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
socketRef.current?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response)
})
closeTab(file.id)
@ -621,11 +622,11 @@ export default function CodeEditor({
setDeletingFolderId(folder.id)
console.log("deleting folder", folder.id)
socket?.emit("getFolder", folder.id, (response: string[]) =>
socketRef.current?.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response)
)
socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
socketRef.current?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
setFiles(response)
setDeletingFolderId("")
})
@ -653,7 +654,7 @@ export default function CodeEditor({
{generate.show && ai ? (
<GenerateInput
user={userData}
socket={socket!}
socket={socketRef.current}
width={generate.width - 90}
data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
@ -713,7 +714,7 @@ export default function CodeEditor({
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
socket={socket!}
socket={socketRef.current}
setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId}
@ -831,7 +832,7 @@ export default function CodeEditor({
open={() => {
usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
}} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
} } collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel

View File

@ -27,7 +27,7 @@ export default function Navbar({
const [isShareOpen, setIsShareOpen] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const isOwner = sandboxData.userId === userData.id;
const isOwner = sandboxData.userId === userData.id;;
return (
<>

View File

@ -8,15 +8,12 @@ import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext"
export default function Terminals() {
const { socket } = useSocket();
const {
terminals,
setTerminals,
socket,
createNewTerminal,
closeTerminal,
activeTerminalId,

View File

@ -1,63 +0,0 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface SocketContextType {
socket: Socket | null;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
setUserAndSandboxId,
};
return (
<SocketContext.Provider value={ value }>
{children}
</SocketContext.Provider>
);
};
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};

View File

@ -1,11 +1,12 @@
"use client";
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
@ -14,16 +15,41 @@ interface TerminalContextType {
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
deploy: (callback: () => void) => void;
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { socket } = useSocket();
const [socket, setSocket] = useState<Socket | null>(null);
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return;
@ -59,6 +85,11 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
};
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket");
console.log("Deploying...")
@ -68,6 +99,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
const value = {
socket,
terminals,
setTerminals,
activeTerminalId,
@ -76,6 +108,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal,
createNewTerminal,
closeTerminal,
setUserAndSandboxId,
deploy
};