Skip to content

Commit 6f1a7c7

Browse files
feat: implement dashboard page for viewing active and past rooms (#18)
* feat(collaboration-service): integrate question service and enhance room management - Added QUESTION_SERVICE_URL to configuration for fetching question details. - Implemented fetchQuestionDetails method in RoomController to retrieve question data from the question service. - Updated getRoomsByUserId method to include question details in the response. - Enhanced roomRoutes to support fetching rooms by user ID with question details. - Updated frontend API client to include a method for retrieving user rooms with question data. - Added a new Dashboard link in the frontend navigation for improved user access. * refactor(dashboard): enhance layout and error handling - Updated DashboardPage layout for improved responsiveness and user experience. - Adjusted error message display and loading indicators for better visibility. - Refactored PastRoomsPanel to include duration formatting and improved structure. - Added formatDuration function to calculate and display the duration between room creation and closure. - Updated API client to include createdAt field in RoomDetails for better data handling. * feat(matching): integrate matching store into practice and dashboard components - Added useMatchingStore to manage matching state in PracticePage, LogoutButton, and ActiveRoomsPanel. - Implemented redirection to the matching page when a user is in a matching state or room is preparing. - Disabled logout and join room buttons when in a matching state to prevent user actions during matching. - Enhanced ActiveRoomsPanel to display partner usernames and improved layout for better user experience. - Sorted active rooms by creation date for better organization. * chore(dependencies): update pnpm-lock.yaml * fix: fix lint errors
1 parent c6505dc commit 6f1a7c7

File tree

17 files changed

+721
-5
lines changed

17 files changed

+721
-5
lines changed

backend/collaboration-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"packageManager": "[email protected]",
1616
"dependencies": {
1717
"@y/websocket-server": "^0.1.1",
18+
"axios": "^1.7.9",
1819
"amqplib": "^0.10.9",
1920
"cors": "^2.8.5",
2021
"express": "^5.1.0",

backend/collaboration-service/src/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const config = {
88
"mongodb://admin:password@localhost:27017/peerprepCollabService?authSource=admin",
99
// Room Management
1010
ROOM_TIMEOUT_MINUTES: parseInt(process.env.ROOM_TIMEOUT_MINUTES) || 10,
11+
// Question service
12+
QUESTION_SERVICE_URL: process.env.QUESTION_SERVICE_URL || "http://localhost:8003",
1113
// Yjs MongoDB Provider Persistence Settings
1214
PERSISTENCE_CONFIG: {
1315
multipleCollections: true, // each document gets an own collection in the database

backend/collaboration-service/src/controllers/roomController.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { v4 as uuidv4 } from "uuid";
2+
import axios from "axios";
23
import db from "../db.js";
4+
import config from "../config.js";
35
import Room from "../models/roomModel.js";
46
import roomManager from "../websocket/roomManager.js";
57

@@ -75,6 +77,85 @@ class RoomController {
7577
}
7678
}
7779

80+
/**
81+
* Fetch question details from question service
82+
* @param {string} questionId - Question ID
83+
* @returns {Promise<Object|null>} - Question details (title, difficulty, topic) or null if not found
84+
*/
85+
async fetchQuestionDetails(questionId) {
86+
try {
87+
const response = await axios.get(
88+
`${config.QUESTION_SERVICE_URL}/v1/questions/${questionId}`,
89+
{ timeout: 5000 }
90+
);
91+
return {
92+
_id: response.data._id,
93+
title: response.data.title,
94+
difficulty: response.data.difficulty,
95+
topic: response.data.topic,
96+
};
97+
} catch (error) {
98+
if (error.response?.status === 404) {
99+
console.warn(`Question ${questionId} not found`);
100+
return null;
101+
}
102+
103+
if (error.response) {
104+
console.error(
105+
`Failed to fetch question ${questionId}:`,
106+
`Status ${error.response.status}`,
107+
error.response.data?.error || error.response.statusText
108+
);
109+
} else if (error.request) {
110+
console.error(
111+
`Failed to fetch question ${questionId}:`,
112+
`No response from question service at ${config.QUESTION_SERVICE_URL}.`,
113+
`Error: ${error.message || 'Network error'}`
114+
);
115+
} else {
116+
console.error(
117+
`Failed to fetch question ${questionId}:`,
118+
error.message || 'Unknown error'
119+
);
120+
}
121+
122+
return null;
123+
}
124+
}
125+
126+
/**
127+
* Get all rooms for a user with question details
128+
* @param {string} userId - User ID
129+
* @returns {Promise<Array<Object>>} - Array of room objects with question details
130+
*/
131+
async getRoomsByUserId(userId) {
132+
try {
133+
const rooms = await Room.findRoomsByUserId(userId);
134+
const roomsArray = rooms.map((room) => room.toObject());
135+
136+
const roomsWithQuestions = await Promise.all(
137+
roomsArray.map(async (room) => {
138+
if (room.questionId) {
139+
const questionDetails = await this.fetchQuestionDetails(room.questionId);
140+
return {
141+
...room,
142+
question: questionDetails,
143+
};
144+
}
145+
return {
146+
...room,
147+
question: null,
148+
};
149+
})
150+
);
151+
152+
return roomsWithQuestions;
153+
} catch (error) {
154+
console.error(`Failed to get rooms for user ${userId}:`, error);
155+
throw error;
156+
}
157+
}
158+
78159
/**
79160
* Get document content from Yjs persistence
80161
* @param {string} roomId - Room ID

backend/collaboration-service/src/http/httpServer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ class HttpServer {
1717
* Set up Express middleware
1818
*/
1919
setupMiddleware() {
20+
2021
this.app.use(
2122
cors({
2223
origin: process.env.WEB_BASE_URL,
2324
credentials: true,
25+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
26+
allowedHeaders: ["Content-Type", "Authorization"],
2427
}),
2528
);
2629
this.app.use(express.json());

backend/collaboration-service/src/http/roomRoutes.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ const router = express.Router();
3434
// }
3535
// });
3636

37+
/**
38+
* GET /api/v1/rooms?userId=<userId>
39+
* Get all rooms for a user
40+
*/
41+
router.get("/", async (req, res) => {
42+
try {
43+
const { userId } = req.query;
44+
45+
if (!userId) {
46+
return res.status(400).json({
47+
success: false,
48+
error: "userId query parameter is required",
49+
});
50+
}
51+
52+
const rooms = await roomController.getRoomsByUserId(userId);
53+
res.json({
54+
success: true,
55+
rooms,
56+
});
57+
} catch (error) {
58+
console.error("Failed to get rooms for user:", error);
59+
res.status(500).json({
60+
success: false,
61+
error: error.message,
62+
});
63+
}
64+
});
65+
3766
/**
3867
* GET /api/v1/rooms/:roomId
3968
* Get room information and document content

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,14 @@ services:
138138
- HTTP_PORT=8004
139139
- WS_PORT=8005
140140
- ROOM_TIMEOUT_MINUTES=10
141+
- QUESTION_SERVICE_URL=http://question-service:8003
141142
- KAFKA_HOST=kafka
142143
- KAFKA_PORT=9092
143144
- KAFKA_BROKERS=kafka:9092
144145
depends_on:
145146
- mongodb
146147
- kafka
148+
- question-service
147149
networks:
148150
- cs3219-peerprep-network
149151
execution-service:
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import Navbar from "@/components/ui/nav-bar";
4+
import ProtectedRoute from "@/components/auth/protected-route";
5+
import ActiveRoomsPanel from "@/components/dashboard/active-rooms-panel";
6+
import PastRoomsPanel from "@/components/dashboard/past-rooms-panel";
7+
import { useDashboardData } from "@/hooks/use-dashboard-data";
8+
9+
export default function DashboardPage() {
10+
const { activeRooms, pastRooms, isLoading, error } = useDashboardData();
11+
12+
return (
13+
<ProtectedRoute>
14+
<div className="h-screen flex flex-col bg-[#d4eaf8] overflow-hidden">
15+
<Navbar />
16+
<main className="flex-1 container mx-auto px-4 py-4 overflow-hidden">
17+
{error && (
18+
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
19+
<p className="font-semibold">Error loading rooms</p>
20+
<p>{error}</p>
21+
</div>
22+
)}
23+
24+
{isLoading ? (
25+
<div className="flex items-center justify-center h-full">
26+
<p className="text-lg text-gray-600">Loading rooms...</p>
27+
</div>
28+
) : (
29+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 h-full min-h-0">
30+
<div className="h-full min-h-0">
31+
<ActiveRoomsPanel rooms={activeRooms} />
32+
</div>
33+
34+
<div className="h-full min-h-0">
35+
<PastRoomsPanel rooms={pastRooms} />
36+
</div>
37+
</div>
38+
)}
39+
</main>
40+
</div>
41+
</ProtectedRoute>
42+
);
43+
}

frontend/src/app/matching/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function MatchingPage() {
1313
const router = useRouter();
1414
const { user, isUser, isLoading } = useAuthContext();
1515
const { refreshAccessToken } = useAuth();
16-
const { roomId, matchFound, initializeSocket, setUser, cleanup } = useMatchingStore();
16+
const { roomId, matchFound, initializeSocket, setUser, cleanup, reset } = useMatchingStore();
1717

1818
useEffect(() => {
1919
if (!isLoading && !isUser) {
@@ -23,9 +23,14 @@ export default function MatchingPage() {
2323

2424
useEffect(() => {
2525
if (matchFound && roomId) {
26-
router.push(`/practice/${roomId}`);
26+
router.push("/dashboard");
27+
// Reset matchFound flag after redirect to allow re-matching
28+
// Reset after a short delay to ensure navigation happens first
29+
setTimeout(() => {
30+
reset();
31+
}, 100);
2732
}
28-
}, [matchFound, roomId, router]);
33+
}, [matchFound, roomId, router, reset]);
2934

3035
useEffect(() => {
3136
if (user) {

frontend/src/app/practice/[id]/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
1212
import { Spinner } from "@/components/ui/spinner";
1313
import { useCollaborationState, useCollaborationActions } from "@/stores/collaboration-store";
1414
import ProtectedRoute from "@/components/auth/protected-route";
15+
import { useMatchingStore } from "@/stores/matching-store";
1516

1617
const CodeEditorPanel = dynamic(() => import("@/components/practice/code-editor-panel"), {
1718
ssr: false,
@@ -25,6 +26,14 @@ export default function PracticePage() {
2526

2627
const { roomDetails, isLoading, error } = useCollaborationState();
2728
const { fetchRoomDetails, fetchQuestionDetails, reset } = useCollaborationActions();
29+
const isMatching = useMatchingStore((state) => state.isMatching);
30+
const isRoomPreparing = useMatchingStore((state) => state.isRoomPreparing);
31+
32+
useEffect(() => {
33+
if (isMatching || isRoomPreparing) {
34+
router.push("/matching");
35+
}
36+
}, [isMatching, isRoomPreparing, router]);
2837

2938
// Fetch room details on mount
3039
useEffect(() => {

frontend/src/components/auth/logout-button.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { AxiosError } from "axios";
44
import { Button } from "@/components/ui/button";
55
import { useAuthContext } from "@/contexts/auth-context";
66
import { authAPI } from "@/lib/api-client";
7+
import { useMatchingStore } from "@/stores/matching-store";
78

89
export default function LogoutButton() {
910
const { logout } = useAuthContext();
11+
const isMatching = useMatchingStore((state) => state.isMatching);
12+
const isRoomPreparing = useMatchingStore((state) => state.isRoomPreparing);
1013

1114
const handleLogout = async () => {
1215
try {
@@ -24,6 +27,7 @@ export default function LogoutButton() {
2427
<Button
2528
variant="logout"
2629
className="text-black"
30+
disabled={isMatching || isRoomPreparing}
2731
onClick={() => {
2832
handleLogout();
2933
}}

0 commit comments

Comments
 (0)