Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
"@emotion/react": "^11.14.0",
"@emotion/style": "^0.8.0",
"@emotion/styled": "^11.14.1",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-query": "^5.90.2",
Expand All @@ -31,15 +35,21 @@
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.54.0",
"motion": "^12.23.22",
"next": "15.5.4",
"node-domexception": "^2.0.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.65.0",
"tailwind-merge": "^3.3.1",
"react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.6",
"react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"y-monaco": "^0.1.6",
"y-websocket": "^3.0.0",
"yjs": "^13.6.27",
"zustand": "^5.0.8"
},
"devDependencies": {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,16 @@
@apply bg-background text-foreground;
}
}

/* Yjs Awareness Cursor Styles */
.yRemoteSelection {
background-color: var(--selection-color, oklch(0.7 0.2 150 / 0.3));
}

.yRemoteSelectionHead {
position: absolute;
border-left: 2px solid var(--cursor-color, oklch(0.7 0.2 150 / 0.3));
border-top: 2px solid var(--cursor-color, oklch(0.7 0.2 150 / 0.3));
border-bottom: 2px solid var(--cursor-color, oklch(0.7 0.2 150 / 0.3));
height: 100%;
}
149 changes: 149 additions & 0 deletions frontend/src/app/practice/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { useEffect } from "react";
import { useRouter, useParams } from "next/navigation";
import dynamic from "next/dynamic";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import Header from "@/components/ui/header";
import QuestionPanel from "@/components/practice/question-panel";
import CommunicationPanel from "@/components/practice/communication-panel";
import CodeOutputPanel from "@/components/practice/code-output-panel";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { useCollaborationState, useCollaborationActions } from "@/stores/collaboration-store";

const CodeEditorPanel = dynamic(() => import("@/components/practice/code-editor-panel"), {
ssr: false,
loading: () => <div className="h-full flex items-center justify-center">Loading editor...</div>,
});

export default function PracticePage() {
const router = useRouter();
const params = useParams();
const roomId = params?.id as string;

const { roomDetails, isLoading, error } = useCollaborationState();
const { fetchRoomDetails, reset } = useCollaborationActions();

// Fetch room details on mount
useEffect(() => {
if (roomId) {
fetchRoomDetails(roomId).catch((err) => {
console.error("Failed to fetch room details:", err);
});
}

// Cleanup on unmount
return () => {
reset();
};
}, [fetchRoomDetails, reset, roomId]);

// Handle leave session
const handleLeaveSession = () => {
// Simply redirect to home page
router.push("/");
};

// Loading state
if (isLoading) {
return (
<div className="h-screen w-full flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Spinner />
<p className="text-muted-foreground">Loading collaboration room...</p>
</div>
</div>
);
}

// Error state
if (error || !roomDetails) {
return (
<div className="h-screen w-full flex items-center justify-center">
<div className="flex flex-col items-center gap-4 text-center">
<p className="text-destructive font-semibold">Failed to load room</p>
<p className="text-muted-foreground">{error || "Room not found"}</p>
<Button onClick={() => router.push("/")}>Go Back Home</Button>
</div>
</div>
);
}

// Read-only mode (room is closed)
const isReadOnly = !roomDetails.isActive;

return (
<div className="h-screen w-full flex flex-col">
<Header>
<div className="flex items-center gap-4">
<Button variant={"destructive"} onClick={handleLeaveSession}>
Leave Room
</Button>
</div>
</Header>

<div className="flex-1">
{isReadOnly ? (
// Read-only layout: Only question and read-only code editor
<ResizablePanelGroup direction="horizontal" className="h-full w-full">
{/* Left Panel */}
<ResizablePanel>
<div className="h-full overflow-y-auto">
<QuestionPanel />
</div>
</ResizablePanel>

<ResizableHandle className="bg-gray-400 hover:bg-gray-600 w-1 cursor-col-resize" />

{/* Right Panel */}
<ResizablePanel>
<div className="h-full overflow-y-auto">
<CodeEditorPanel readOnly={true} />
</div>
</ResizablePanel>
</ResizablePanelGroup>
) : (
// Full layout with all panels
<ResizablePanelGroup direction="horizontal" className="h-full w-full">
{/* Left Panel */}
<ResizablePanel>
<ResizablePanelGroup direction="vertical">
<ResizablePanel>
<div className="h-full overflow-y-auto">
<QuestionPanel />
</div>
</ResizablePanel>
<ResizableHandle className="bg-gray-400 hover:bg-gray-600 w-1 cursor-col-resize" />
<ResizablePanel>
<div className="h-full overflow-y-auto">
<CommunicationPanel />
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>

<ResizableHandle className="bg-gray-400 hover:bg-gray-600 w-1 cursor-col-resize" />

{/* Right Panel */}
<ResizablePanel>
<ResizablePanelGroup direction="vertical">
<ResizablePanel>
<div className="h-full overflow-y-auto">
<CodeEditorPanel readOnly={false} />
</div>
</ResizablePanel>
<ResizableHandle className="bg-gray-400 hover:bg-gray-600 w-1 cursor-col-resize" />
<ResizablePanel>
<div className="h-full overflow-y-auto">
<CodeOutputPanel />
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
</div>
);
}
Loading