From 57050a3c7694f252dd7597bd6f95f321e4552343 Mon Sep 17 00:00:00 2001 From: rogutkuba Date: Thu, 31 Jul 2025 16:54:15 -0400 Subject: [PATCH 01/16] basic sandpack ui --- apps/web/package.json | 2 + apps/web/src/components/room/CodeOutput.tsx | 245 ++++++------ apps/web/src/components/room/Dockview.tsx | 188 ++++----- .../components/room/guest/GuestRoomView.tsx | 300 +++++++------- .../src/components/room/host/HostRoomView.tsx | 179 +++++---- .../components/room/output/SandpackOutput.tsx | 208 ++++++++++ apps/web/src/contexts/SandpackContext.tsx | 158 ++++++++ apps/web/src/query/error.query.tsx | 8 +- pnpm-lock.yaml | 369 ++++++++++++++++++ 9 files changed, 1220 insertions(+), 437 deletions(-) create mode 100644 apps/web/src/components/room/output/SandpackOutput.tsx create mode 100644 apps/web/src/contexts/SandpackContext.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 45303bc..7533846 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,8 @@ "dependencies": { "@cloudflare/vite-plugin": "^1.7.5", "@coderscreen/ui": "workspace:^", + "@codesandbox/sandpack-client": "^2.19.8", + "@codesandbox/sandpack-react": "^2.20.0", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-checkbox": "^1.1.4", diff --git a/apps/web/src/components/room/CodeOutput.tsx b/apps/web/src/components/room/CodeOutput.tsx index c8d4a9e..9e38787 100644 --- a/apps/web/src/components/room/CodeOutput.tsx +++ b/apps/web/src/components/room/CodeOutput.tsx @@ -1,121 +1,140 @@ -import { RiArrowRightSLine, RiHammerLine, RiPlayLine, RiTerminalLine } from '@remixicon/react'; -import { useEffect, useRef, useState } from 'react'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { Tooltip } from '@/components/ui/tooltip'; -import { cn } from '@/lib/utils'; -import { useCodeExecutionHistory } from '@/query/realtime/execution.query'; +import { + RiArrowRightSLine, + RiHammerLine, + RiPlayLine, + RiTerminalLine, +} from "@remixicon/react"; +import { useEffect, useRef, useState } from "react"; +import { SandpackOutput } from "@/components/room/output/SandpackOutput"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Tooltip } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { useCodeExecutionHistory } from "@/query/realtime/execution.query"; export const CodeOutput = () => { - const { history } = useCodeExecutionHistory(); - const [openItems, setOpenItems] = useState>(new Map()); - const scrollContainerRef = useRef(null); + return ; +}; + +export const _CodeOutput = () => { + const { history } = useCodeExecutionHistory(); + const [openItems, setOpenItems] = useState>(new Map()); + const scrollContainerRef = useRef(null); - // Auto-scroll to bottom when new items are added - useEffect(() => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; - } - }, []); + // Auto-scroll to bottom when new items are added + useEffect(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTop = + scrollContainerRef.current.scrollHeight; + } + }, []); - const toggleItem = (index: number) => { - setOpenItems((prev) => { - const newOpen = new Map(prev); - const val = newOpen.get(index); - newOpen.set(index, val === undefined ? false : !val); - return newOpen; - }); - }; + const toggleItem = (index: number) => { + setOpenItems((prev) => { + const newOpen = new Map(prev); + const val = newOpen.get(index); + newOpen.set(index, val === undefined ? false : !val); + return newOpen; + }); + }; - if (!history.length) { - return ( -
-
-
-
-
- -
-

No output yet

-

- Run your code to see the execution results and output here -

-
-
-
-
- ); - } + if (!history.length) { + return ( +
+
+
+
+
+ +
+

+ No output yet +

+

+ Run your code to see the execution results and output here +

+
+
+
+
+ ); + } - return ( -
-
- {history.map((data, idx) => { - const hasOutput = data.stdout && data.stdout.trim() !== ''; - const hasError = data.stderr && data.stderr.trim() !== ''; - // if never opened or has been opened - const isOpen = !openItems.has(idx) || openItems.get(idx) === true; - const executionNumber = idx + 1; // Latest is at the bottom + return ( +
+
+ {history.map((data, idx) => { + const hasOutput = data.stdout && data.stdout.trim() !== ""; + const hasError = data.stderr && data.stderr.trim() !== ""; + // if never opened or has been opened + const isOpen = !openItems.has(idx) || openItems.get(idx) === true; + const executionNumber = idx + 1; // Latest is at the bottom - return ( - toggleItem(idx)} - > - -
- - - -
- Execution #{executionNumber} - - {new Date(data.timestamp).toLocaleTimeString()} - -
-
-
- {data.compileTime && ( - - - - {data.compileTime}ms - - - )} - {data.elapsedTime >= 0 && ( - - - - {data.elapsedTime}ms - - - )} -
-
- -
- {hasOutput && ( -
-                      {data.stdout}
-                    
- )} - {hasError && ( -
-                      Error: {data.stderr}
-                    
- )} -
-
-
- ); - })} -
-
- ); + return ( + toggleItem(idx)} + > + +
+ + + +
+ + Execution #{executionNumber} + + + {new Date(data.timestamp).toLocaleTimeString()} + +
+
+
+ {data.compileTime && ( + + + + {data.compileTime}ms + + + )} + {data.elapsedTime >= 0 && ( + + + + {data.elapsedTime}ms + + + )} +
+
+ +
+ {hasOutput && ( +
+											{data.stdout}
+										
+ )} + {hasError && ( +
+											Error: {data.stderr}
+										
+ )} +
+
+
+ ); + })} +
+
+ ); }; diff --git a/apps/web/src/components/room/Dockview.tsx b/apps/web/src/components/room/Dockview.tsx index 83545c5..71a068e 100644 --- a/apps/web/src/components/room/Dockview.tsx +++ b/apps/web/src/components/room/Dockview.tsx @@ -1,109 +1,113 @@ import { - RiChatAiLine, - RiCodeLine, - RiPencilLine, - RiSlideshowLine, - RiStickyNoteLine, - RiTerminalLine, -} from '@remixicon/react'; -import { DockviewTheme, IDockviewPanelHeaderProps, IDockviewPanelProps } from 'dockview'; -import { useMemo } from 'react'; -import { AiChatView } from '@/components/room/ai-chat/AiChatView'; -import { CodeOutput } from '@/components/room/CodeOutput'; -import { CodeEditor } from '@/components/room/editor/CodeEditor'; -import { InstructionEditor } from '@/components/room/tiptap/InstructionEditor'; -import { NotesEditor } from '@/components/room/tiptap/NotesEditor'; -import { WhiteboardView } from '@/components/room/whiteboard/WhiteboardView'; -import { cx } from '@/lib/utils'; + RiChatAiLine, + RiCodeLine, + RiPencilLine, + RiSlideshowLine, + RiStickyNoteLine, + RiTerminalLine, +} from "@remixicon/react"; +import type { + DockviewTheme, + IDockviewPanelHeaderProps, + IDockviewPanelProps, +} from "dockview"; +import { useMemo } from "react"; +import { AiChatView } from "@/components/room/ai-chat/AiChatView"; +import { CodeOutput } from "@/components/room/CodeOutput"; +import { CodeEditor } from "@/components/room/editor/CodeEditor"; +import { InstructionEditor } from "@/components/room/tiptap/InstructionEditor"; +import { NotesEditor } from "@/components/room/tiptap/NotesEditor"; +import { WhiteboardView } from "@/components/room/whiteboard/WhiteboardView"; +import { cx } from "@/lib/utils"; export const DOCKVIEW_PANEL_IDS = { - CODE_EDITOR: 'code-editor', - INSTRUCTIONS: 'instructions', - PROGRAM_OUTPUT: 'program-output', - WHITEBOARD: 'whiteboard', - AI_CHAT: 'ai-chat', - NOTES: 'notes', - TAB: 'tab', + CODE_EDITOR: "code-editor", + INSTRUCTIONS: "instructions", + CODE_OUTPUT: "code-output", + WHITEBOARD: "whiteboard", + AI_CHAT: "ai-chat", + NOTES: "notes", + TAB: "tab", }; export const lightDockviewTheme: DockviewTheme = { - name: 'light', - className: 'dockview-theme-light', + name: "light", + className: "dockview-theme-light", }; // Common tab icons function export const getTabIcon = (panelId: string) => { - switch (panelId) { - case DOCKVIEW_PANEL_IDS.CODE_EDITOR: - return ; - case DOCKVIEW_PANEL_IDS.INSTRUCTIONS: - return ; - case DOCKVIEW_PANEL_IDS.PROGRAM_OUTPUT: - return ; - case DOCKVIEW_PANEL_IDS.WHITEBOARD: - return ; - case DOCKVIEW_PANEL_IDS.AI_CHAT: - return ; - case DOCKVIEW_PANEL_IDS.NOTES: - return ; - default: - return null; - } + switch (panelId) { + case DOCKVIEW_PANEL_IDS.CODE_EDITOR: + return ; + case DOCKVIEW_PANEL_IDS.INSTRUCTIONS: + return ; + case DOCKVIEW_PANEL_IDS.CODE_OUTPUT: + return ; + case DOCKVIEW_PANEL_IDS.WHITEBOARD: + return ; + case DOCKVIEW_PANEL_IDS.AI_CHAT: + return ; + case DOCKVIEW_PANEL_IDS.NOTES: + return ; + default: + return null; + } }; // Common components mapping export const useDockviewComponents = (isGuest: boolean) => - useMemo( - () => ({ - 'code-editor': (_: IDockviewPanelProps) => ( -
- -
- ), - instructions: (_: IDockviewPanelProps) => ( -
- -
- ), - 'program-output': (_: IDockviewPanelProps) => ( -
- -
- ), - whiteboard: (_: IDockviewPanelProps) => ( -
- -
- ), - 'ai-chat': (_: IDockviewPanelProps) => ( -
- -
- ), - notes: (_: IDockviewPanelProps) => ( -
- -
- ), - }), - [isGuest] - ); + useMemo( + () => ({ + "code-editor": (_: IDockviewPanelProps) => ( +
+ +
+ ), + instructions: (_: IDockviewPanelProps) => ( +
+ +
+ ), + "code-output": (_: IDockviewPanelProps) => ( +
+ +
+ ), + whiteboard: (_: IDockviewPanelProps) => ( +
+ +
+ ), + "ai-chat": (_: IDockviewPanelProps) => ( +
+ +
+ ), + notes: (_: IDockviewPanelProps) => ( +
+ +
+ ), + }), + [isGuest], + ); // Common tab components export const useTabComponents = () => - useMemo( - () => ({ - tab: (props: IDockviewPanelHeaderProps) => ( -
- {getTabIcon(props.api.id)} - {props.api.title} -
- ), - }), - [] - ); + useMemo( + () => ({ + tab: (props: IDockviewPanelHeaderProps) => ( +
+ {getTabIcon(props.api.id)} + {props.api.title} +
+ ), + }), + [], + ); diff --git a/apps/web/src/components/room/guest/GuestRoomView.tsx b/apps/web/src/components/room/guest/GuestRoomView.tsx index a366838..b05004a 100644 --- a/apps/web/src/components/room/guest/GuestRoomView.tsx +++ b/apps/web/src/components/room/guest/GuestRoomView.tsx @@ -1,153 +1,161 @@ -import { DockviewReact } from 'dockview'; -import { useEffect, useState } from 'react'; -import { GuestRoomHeader } from '@/components/room/guest/GuestRoomHeader'; -import { RoomFooter } from '@/components/room/RoomFooter'; -import { RoomProvider, useRoomContext } from '@/contexts/RoomContext'; -import { Guest, getGuest, setGuest } from '@/lib/guest'; -import { usePublicRoom } from '@/query/publicRoom.query'; -import { getRandomColor } from '@/query/realtime/utils'; +import { DockviewReact } from "dockview"; +import { useEffect, useState } from "react"; +import { GuestRoomHeader } from "@/components/room/guest/GuestRoomHeader"; +import { RoomFooter } from "@/components/room/RoomFooter"; +import { RoomProvider, useRoomContext } from "@/contexts/RoomContext"; +import { SandpackProvider } from "@/contexts/SandpackContext"; +import { type Guest, getGuest, setGuest } from "@/lib/guest"; +import { usePublicRoom } from "@/query/publicRoom.query"; +import { getRandomColor } from "@/query/realtime/utils"; import { - DOCKVIEW_PANEL_IDS, - lightDockviewTheme, - useDockviewComponents, - useTabComponents, -} from '../Dockview'; -import { GuestStartView } from './GuestStartView'; -import { GuestSummaryView } from './GuestSummaryView'; + DOCKVIEW_PANEL_IDS, + lightDockviewTheme, + useDockviewComponents, + useTabComponents, +} from "../Dockview"; +import { GuestStartView } from "./GuestStartView"; +import { GuestSummaryView } from "./GuestSummaryView"; export const GuestRoomView = () => { - const [guestInfo, setGuestInfo] = useState(null); - const { publicRoom, isLoading } = usePublicRoom(); - - // Load guest info from localStorage on mount - useEffect(() => { - if (!guestInfo) { - const guest = getGuest(); - if (guest) { - setGuestInfo(guest); - } - } - }, [guestInfo]); - - const handleJoinAsGuest = (name: string, email: string) => { - const newGuest = { id: crypto.randomUUID(), name, color: getRandomColor(name), email }; - setGuest(newGuest); - setGuestInfo(newGuest); - }; - - // Show loading state while room data is being fetched - if (isLoading) { - return ( -
-
-

Loading room...

-
-
- ); - } - - // If room is completed, show summary view regardless of guest info - if (publicRoom?.status === 'completed') { - return ; - } - - // If room is not active, show start view with appropriate message or If no guest info and room is active, show the start view - if (publicRoom?.status !== 'active' || !guestInfo) { - return ; - } - - // If guest info exists and room is active, show the room content - return ( - - - - ); + const [guestInfo, setGuestInfo] = useState(null); + const { publicRoom, isLoading } = usePublicRoom(); + + // Load guest info from localStorage on mount + useEffect(() => { + if (!guestInfo) { + const guest = getGuest(); + if (guest) { + setGuestInfo(guest); + } + } + }, [guestInfo]); + + const handleJoinAsGuest = (name: string, email: string) => { + const newGuest = { + id: crypto.randomUUID(), + name, + color: getRandomColor(name), + email, + }; + setGuest(newGuest); + setGuestInfo(newGuest); + }; + + // Show loading state while room data is being fetched + if (isLoading) { + return ( +
+
+

Loading room...

+
+
+ ); + } + + // If room is completed, show summary view regardless of guest info + if (publicRoom?.status === "completed") { + return ; + } + + // If room is not active, show start view with appropriate message or If no guest info and room is active, show the start view + if (publicRoom?.status !== "active" || !guestInfo) { + return ; + } + + // If guest info exists and room is active, show the room content + return ( + + + + + + ); }; const GuestRoomContent = () => { - const [currentView, setCurrentView] = useState<'room' | 'summary'>('room'); - const { subscribeToStatus, currentStatus } = useRoomContext(); - - // Subscribe to live status changes - useEffect(() => { - const unsubscribe = subscribeToStatus((newStatus) => { - console.log('Room status changed to:', newStatus); - - // Update view based on status change - if (newStatus === 'completed') { - setCurrentView('summary'); - } else if (newStatus === 'active') { - setCurrentView('room'); - } - }); - - return unsubscribe; - }, [subscribeToStatus]); - - const components = useDockviewComponents(true); - const tabComponents = useTabComponents(); - - // If status is completed, show summary view - if (currentStatus === 'completed' || currentView === 'summary') { - return ; - } - - // Show the room content - return ( -
- -
- { - const { api } = event; - - // Add code editor panel - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.CODE_EDITOR, - component: 'code-editor', - title: 'Code Editor', - tabComponent: 'tab', - }); - - // Add other panels as tabs in a second panel - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.INSTRUCTIONS, - component: 'instructions', - title: 'Instructions', - tabComponent: 'tab', - position: { - direction: 'right', - referencePanel: 'code-editor', - }, - }); - - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.PROGRAM_OUTPUT, - component: 'program-output', - title: 'Program Output', - tabComponent: 'tab', - }); - - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.WHITEBOARD, - component: 'whiteboard', - title: 'Whiteboard', - tabComponent: 'tab', - }); - - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.AI_CHAT, - component: 'ai-chat', - title: 'AI Chat', - tabComponent: 'tab', - }); - }} - /> -
- -
- ); + const [currentView, setCurrentView] = useState<"room" | "summary">("room"); + const { subscribeToStatus, currentStatus } = useRoomContext(); + + // Subscribe to live status changes + useEffect(() => { + const unsubscribe = subscribeToStatus((newStatus) => { + console.log("Room status changed to:", newStatus); + + // Update view based on status change + if (newStatus === "completed") { + setCurrentView("summary"); + } else if (newStatus === "active") { + setCurrentView("room"); + } + }); + + return unsubscribe; + }, [subscribeToStatus]); + + const components = useDockviewComponents(true); + const tabComponents = useTabComponents(); + + // If status is completed, show summary view + if (currentStatus === "completed" || currentView === "summary") { + return ; + } + + // Show the room content + return ( +
+ +
+ { + const { api } = event; + + // Add code editor panel + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.CODE_EDITOR, + component: "code-editor", + title: "Code Editor", + tabComponent: "tab", + }); + + // Add other panels as tabs in a second panel + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.INSTRUCTIONS, + component: "instructions", + title: "Instructions", + tabComponent: "tab", + position: { + direction: "right", + referencePanel: "code-editor", + }, + }); + + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.CODE_OUTPUT, + component: "code-output", + title: "Code Output", + tabComponent: "tab", + }); + + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.WHITEBOARD, + component: "whiteboard", + title: "Whiteboard", + tabComponent: "tab", + }); + + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.AI_CHAT, + component: "ai-chat", + title: "AI Chat", + tabComponent: "tab", + }); + }} + /> +
+ +
+ ); }; diff --git a/apps/web/src/components/room/host/HostRoomView.tsx b/apps/web/src/components/room/host/HostRoomView.tsx index 71820ab..5fd68c2 100644 --- a/apps/web/src/components/room/host/HostRoomView.tsx +++ b/apps/web/src/components/room/host/HostRoomView.tsx @@ -1,102 +1,111 @@ -import { RiLockLine } from '@remixicon/react'; -import { DockviewReact } from 'dockview'; -import { HostRoomHeader } from '@/components/room/host/HostRoomHeader'; -import { RoomFooter } from '@/components/room/RoomFooter'; -import { RoomProvider, useRoomContext } from '@/contexts/RoomContext'; +import { RiLockLine } from "@remixicon/react"; +import { DockviewReact } from "dockview"; +import { HostRoomHeader } from "@/components/room/host/HostRoomHeader"; +import { RoomFooter } from "@/components/room/RoomFooter"; +import { RoomProvider, useRoomContext } from "@/contexts/RoomContext"; +import { SandpackProvider } from "@/contexts/SandpackContext"; import { - DOCKVIEW_PANEL_IDS, - lightDockviewTheme, - useDockviewComponents, - useTabComponents, -} from '../Dockview'; + DOCKVIEW_PANEL_IDS, + lightDockviewTheme, + useDockviewComponents, + useTabComponents, +} from "../Dockview"; export const HostRoomView = () => { - return ( - - - - ); + return ( + + + + + + ); }; const HostRoomContent = () => { - const { currentStatus } = useRoomContext(); + const { currentStatus } = useRoomContext(); - const components = useDockviewComponents(false); - const tabComponents = useTabComponents(); + const components = useDockviewComponents(false); + const tabComponents = useTabComponents(); - return ( -
- + return ( +
+ - {/* Read-only banner */} - {currentStatus === 'completed' && ( -
- - - Interview ended - This room is now in read-only mode except for your notes. - -
- )} + {/* Read-only banner */} + {currentStatus === "completed" && ( +
+ + + Interview ended - This room is now in read-only mode except for your + notes. + +
+ )} -
- { - const { api } = event; +
+ { + const { api } = event; - // Add code editor panel - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.CODE_EDITOR, - component: 'code-editor', // Use the original component names - title: 'Code Editor', - tabComponent: 'tab', - }); + // Add code editor panel + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.CODE_EDITOR, + component: "code-editor", // Use the original component names + title: "Code Editor", + tabComponent: "tab", + }); - // Add other panels as tabs in a second panel - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.INSTRUCTIONS, - component: 'instructions', - title: 'Instructions', - tabComponent: 'tab', - position: { - direction: 'right', - referencePanel: 'code-editor', - }, - }); + // Add other panels as tabs in a second panel + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.CODE_OUTPUT, + component: "code-output", + title: "Code Output", + tabComponent: "tab", + renderer: "always", + position: { + direction: "right", + referencePanel: "code-editor", + }, + }); - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.PROGRAM_OUTPUT, - component: 'program-output', - title: 'Program Output', - tabComponent: 'tab', - }); + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.INSTRUCTIONS, + component: "instructions", + title: "Instructions", + tabComponent: "tab", + inactive: true, + }); - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.WHITEBOARD, - component: 'whiteboard', - title: 'Whiteboard', - tabComponent: 'tab', - }); + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.WHITEBOARD, + component: "whiteboard", + title: "Whiteboard", + tabComponent: "tab", + inactive: true, + }); - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.AI_CHAT, - component: 'ai-chat', - title: 'AI Chat', - tabComponent: 'tab', - }); + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.AI_CHAT, + component: "ai-chat", + title: "AI Chat", + tabComponent: "tab", + inactive: true, + }); - api.addPanel({ - id: DOCKVIEW_PANEL_IDS.NOTES, - component: 'notes', - title: 'Notes', - tabComponent: 'tab', - }); - }} - /> -
- -
- ); + api.addPanel({ + id: DOCKVIEW_PANEL_IDS.NOTES, + component: "notes", + title: "Notes", + tabComponent: "tab", + inactive: true, + }); + }} + /> +
+ +
+ ); }; diff --git a/apps/web/src/components/room/output/SandpackOutput.tsx b/apps/web/src/components/room/output/SandpackOutput.tsx new file mode 100644 index 0000000..8494989 --- /dev/null +++ b/apps/web/src/components/room/output/SandpackOutput.tsx @@ -0,0 +1,208 @@ +"use client"; + +import type { ClientOptions, SandboxSetup } from "@codesandbox/sandpack-client"; +import { + RiDeleteBin6Line, + RiProhibitedLine, + RiResetRightLine, +} from "@remixicon/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import { Button } from "@/components/ui/button"; +import { Tooltip } from "@/components/ui/tooltip"; +import { useSandpackContext } from "@/contexts/SandpackContext"; + +export const SandpackOutput = () => { + const iframeRef = useRef(null); + const { sandpackClient, sandpackLoading, initializeSandpackClient } = + useSandpackContext(); + + // Files, environment and dependencies + const content: SandboxSetup = useMemo( + () => ({ + files: { + "/package.json": { + code: JSON.stringify({ + name: "basic-react-app", + main: "src/index.js", + dependencies: { + react: "latest", + "react-dom": "latest", + }, + }), + }, + "/public/index.html": { + code: ` + + + + + Basic React App + + +
+ + +`, + }, + "/src/index.js": { + code: `import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +const root = createRoot(document.getElementById("root")); +root.render(); +`, + }, + "/src/App.js": { + code: `import React from "react"; + +export default function App() { + return ( +
+

Hello, React!

+

This is a basic React app running in Sandpack.

+ +
+ ); +} +`, + }, + }, + template: "create-react-app", + }), + [], + ); + + // Optional options + const options: ClientOptions = { + showOpenInCodeSandbox: false, + showErrorScreen: true, + showLoadingScreen: true, + }; + + // Initialize client once when component mounts + useEffect(() => { + if (!iframeRef.current) { + return; + } + + const loadClient = async () => { + if (!iframeRef.current) return; + await initializeSandpackClient(iframeRef.current, content, options); + }; + + loadClient(); + }, [content, initializeSandpackClient]); + + // Determine loading state based on client status + const getLoadingState = () => { + if (!sandpackClient) { + return "client-initializing-state"; + } + return sandpackClient.status || "unknown"; + }; + + return ( +
+ {sandpackLoading && ( +
+
+
+

{getLoadingState()}

+
+
+ )} + +
+ + + +
+ + + +