diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index a4d5089..8c8f6c8 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -27,6 +27,7 @@ import { AlertCircle, FolderOpen, } from "lucide-react"; + import { CollaborationAvatars } from "@/modules/playground/components/collaboration-avatars"; import { TemplateFileTree } from "@/modules/playground/components/playground-explorer"; import { usePlayground } from "@/modules/playground/hooks/usePlayground"; @@ -37,7 +38,7 @@ import { useParams } from "next/navigation"; import WebContainerPreview from "@/modules/webcontainers/components/webcontainer-preview"; import { useWebContainer } from "@/modules/webcontainers/hooks/useWebContainer"; import { useFileExplorer } from "@/modules/playground/hooks/useFileExplorer"; - +import { fetchCollabToken, getOrCreateYDoc } from "@/lib/yjs"; import { TemplateFile, TemplateFolder, @@ -77,11 +78,13 @@ const MainPlaygroundPage = ({ initialData, id }: MainPlaygroundPageProps) => { const [templateData, setTemplateDataState] = useState(parsedTemplate); const [error] = useState(null); const { saveTemplateData } = usePlayground(id); + const { toggleChat } = useAI(); const [isPreviewVisible, setIsPreviewVisible] = useState(false); const [showAISettings, setShowAISettings] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [isDeployDialogOpen, setIsDeployDialogOpen] = useState(false); const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 }); +const [collaboratorCount, setCollaboratorCount] = useState(0); const sidebar = useSidebar(); const { @@ -113,35 +116,78 @@ const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 }); useEffect(() => { - setPlaygroundId(id); - if (templateData && !openFiles.length) { - setTemplateData(templateData); + setPlaygroundId(id); + if (templateData && !openFiles.length) { + setTemplateData(templateData); + } +}, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); + +// Collaborator tracking +useEffect(() => { + if (!id) return; + + let cancelled = false; + let cleanup = () => {}; + + void (async () => { + try { + const token = await fetchCollabToken(id); + const { provider } = getOrCreateYDoc(id, token); + + if (cancelled) return; + + const updateCollaborators = () => { + if (cancelled) return; + const states = Array.from(provider.awareness.getStates().values()); + const activeUsers = states + .filter((s: any) => s.user) + .map((s: any) => s.user); + const uniqueUsers = Array.from( + new Map(activeUsers.map((u: any) => [u.name, u])).values() + ); + setCollaboratorCount(uniqueUsers.length); + }; + + provider.awareness.on("change", updateCollaborators); + updateCollaborators(); + + cleanup = () => { + provider.awareness.off("change", updateCollaborators); + }; + } catch (error) { + console.error("Failed to track collaborators:", error); } - }, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); + })(); - // Auto-open default file when preview is shown if no file is open - useEffect(() => { - if (isPreviewVisible && !activeFileId && templateData) { - const findDefaultFile = (items: any[]): TemplateFile | null => { - for (const item of items) { - if (!("folderName" in item)) { - if (["App.tsx", "App.jsx", "index.tsx", "index.jsx", "index.js", "main.tsx", "main.js", "index.html"].includes(`${item.filename}.${item.fileExtension}`)) { - return item; - } - } else { - const found = findDefaultFile(item.items); - if (found) return found; + return () => { + cancelled = true; + cleanup(); + }; +}, [id]); + +// Auto-open default file when preview is shown if no file is open +useEffect(() => { + if (isPreviewVisible && !activeFileId && templateData) { + const findDefaultFile = (items: any[]): TemplateFile | null => { + for (const item of items) { + if (!("folderName" in item)) { + if (["App.tsx", "App.jsx", "index.tsx", "index.jsx", "index.js", "main.tsx", "main.js", "index.html"].includes(`${item.filename}.${item.fileExtension}`)) { + return item; } + } else { + const found = findDefaultFile(item.items); + if (found) return found; } - return null; - }; - - const defaultFile = findDefaultFile(templateData.items); - if (defaultFile) { - openFile(defaultFile); } + return null; + }; + + const defaultFile = findDefaultFile(templateData.items); + if (defaultFile) { + openFile(defaultFile); } - }, [isPreviewVisible, activeFileId, templateData, openFile]); + } +}, [isPreviewVisible, activeFileId, templateData, openFile]); // Create wrapper functions that pass saveTemplateData const wrappedHandleAddFile = useCallback( @@ -301,7 +347,7 @@ if (!playgroundData && !templateData && !error) { handleDownloadZip={handleDownloadZip} setShowAISettings={setShowAISettings} closeAllFiles={closeAllFiles} - toggleAIChat={() => useAI.getState().toggleChat()} + toggleAIChat={toggleChat} /> {/* ==== CONTENT ==== */} @@ -366,7 +412,7 @@ if (!playgroundData && !templateData && !error) { setIsPreviewVisible(true)} - onOpenAI={() => useAI.getState().toggleChat()} + onOpenAI={toggleChat} onDownload={handleDownloadZip} onOpenCommandPalette={() => setIsCommandPaletteOpen(true)} /> @@ -378,7 +424,7 @@ if (!playgroundData && !templateData && !error) { activeFile={activeFile} cursorPosition={cursorPosition} containerStatus={containerStatus} - collaboratorCount={0} + collaboratorCount={collaboratorCount} openFileCount={openFiles.length} /> @@ -402,7 +448,7 @@ if (!playgroundData && !templateData && !error) { onSaveAll={handleSaveAll} onDownload={handleDownloadZip} onTogglePreview={() => setIsPreviewVisible((prev) => !prev)} - onToggleAI={() => useAI.getState().toggleChat()} + onToggleAI={toggleChat} onToggleSidebar={() => sidebar.toggleSidebar()} onOpenSettings={() => setShowAISettings(true)} onCloseAllFiles={closeAllFiles}