Skip to content

Commit 9ae0f15

Browse files
authored
Merge pull request #94 from CS3219-AY2425S1/nephelite-patch-3
Implement Various UI Changes
2 parents e207858 + 8669766 commit 9ae0f15

File tree

11 files changed

+320
-73
lines changed

11 files changed

+320
-73
lines changed

backend/question-service/controllers/historyController.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Response } from 'express';
2-
import jwt, { JwtPayload } from 'jsonwebtoken';
32
import historyEntryModel from '../models/HistoryEntry';
43
import { AuthenticatedRequest } from 'middlewares/auth';
54

@@ -37,12 +36,13 @@ export const getUserHistoryEntries = async (req: AuthenticatedRequest, res: Resp
3736
return {
3837
id: entry._id,
3938
key: entry._id,
39+
roomId: entry.roomId,
4040
attemptStartedAt: entry.attemptStartedAt.getTime(),
4141
attemptCompletedAt: entry.attemptCompletedAt.getTime(),
4242
title: entry.question.title,
4343
difficulty: entry.question.difficulty,
4444
topics: entry.question.categories.map((cat: any) => cat.name),
45-
attemptCode: entry.attemptCode,
45+
attemptCodes: entry.attemptCodes,
4646
}});
4747
res.status(200).json(historyViewModels);
4848
} catch (error) {
@@ -71,7 +71,7 @@ export const createOrUpdateUserHistoryEntry = async (req: AuthenticatedRequest,
7171
existingEntry.attemptStartedAt = attemptStartedAt;
7272
existingEntry.attemptCompletedAt = attemptCompletedAt;
7373
existingEntry.collaboratorId = collaboratorId;
74-
existingEntry.attemptCode = attemptCode;
74+
existingEntry.attemptCodes.push(attemptCode);
7575

7676
const updatedEntry = await existingEntry.save();
7777

@@ -84,7 +84,7 @@ export const createOrUpdateUserHistoryEntry = async (req: AuthenticatedRequest,
8484
attemptStartedAt,
8585
attemptCompletedAt,
8686
collaboratorId,
87-
attemptCode,
87+
attemptCodes: [attemptCode],
8888
});
8989

9090
const savedEntry = await newHistoryEntry.save();
@@ -96,6 +96,30 @@ export const createOrUpdateUserHistoryEntry = async (req: AuthenticatedRequest,
9696
}
9797
};
9898

99+
export const removeRoomIdPresence = async (req: AuthenticatedRequest, res: Response) => {
100+
try {
101+
const userId = extractUserIdFromToken(req);
102+
103+
if (!userId) {
104+
return res.status(401).json({ error: 'Invalid or missing token' });
105+
}
106+
const { roomId } = req.params;
107+
108+
const existingEntries = await historyEntryModel.find({ roomId });
109+
const updatedEntries: string[] = [];
110+
111+
existingEntries.forEach(async (entry) => {
112+
entry.roomId = "";
113+
await entry.save();
114+
updatedEntries.push(entry._id.toString());
115+
});
116+
117+
return res.status(200).json({ updatedEntries })
118+
} catch (error) {
119+
return res.status(500).json({ error: getErrorMessage(error) });
120+
}
121+
}
122+
99123
export const deleteUserHistoryEntry = async (req: AuthenticatedRequest, res: Response) => {
100124
try {
101125
const userId = extractUserIdFromToken(req);

backend/question-service/models/HistoryEntry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface HistoryEntry extends mongoose.Document {
99
attemptStartedAt: Date;
1010
attemptCompletedAt: Date;
1111
collaboratorId: string;
12-
attemptCode: string;
12+
attemptCodes: string[];
1313
}
1414

1515
const historyEntrySchema: Schema = new Schema<HistoryEntry>({
@@ -19,7 +19,7 @@ const historyEntrySchema: Schema = new Schema<HistoryEntry>({
1919
attemptStartedAt: { type: Date, required: true, default: Date.now() },
2020
attemptCompletedAt: { type: Date, required: true, default: Date.now() },
2121
collaboratorId: { type: String, required: true },
22-
attemptCode: { type: String, required: true },
22+
attemptCodes: [{ type: String, required: true }],
2323
});
2424

2525
const historyEntryModel = mongoose.model<HistoryEntry>('historyEntry', historyEntrySchema);

backend/question-service/routes/historyRoutes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
deleteUserHistoryEntry,
66
deleteUserHistoryEntries,
77
deleteAllUserHistoryEntries,
8+
removeRoomIdPresence,
89
} from '../controllers/historyController';
910
import { authenticateToken } from '../middlewares/auth';
1011

1112
const router = express.Router();
1213

1314
router.get("/", authenticateToken, getUserHistoryEntries);
1415
router.post("/", authenticateToken, createOrUpdateUserHistoryEntry);
16+
router.post("/room/:id", authenticateToken, removeRoomIdPresence);
1517
router.delete("/user/:id", authenticateToken, deleteUserHistoryEntry);
1618
router.delete("/user", authenticateToken, deleteUserHistoryEntries);
1719
router.delete("/all", authenticateToken, deleteAllUserHistoryEntries);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
export interface HistoryEntry {
22
_id: string;
33
key: string;
4+
roomId: string;
45
attemptStartedAt: string;
56
attemptCompletedAt: string;
67
title: string;
78
difficulty: 'Easy' | 'Medium' | 'Hard';
89
topics: string[];
9-
attemptCode: string;
10+
attemptCodes: string[];
1011
}

frontend/src/presentation/components/CodeEditor/CodeEditor.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useEffect, useRef } from "react";
22
import styles from "./CodeEditor.module.css";
33
import Editor from "@monaco-editor/react";
4-
import { Button, Spin } from "antd";
4+
import { Button, Spin, Modal } from "antd";
55
import { useCollaboration } from "domain/context/CollaborationContext";
66
import * as monaco from "monaco-editor";
77
import { SunOutlined, MoonFilled } from "@ant-design/icons";
@@ -16,6 +16,14 @@ interface CodeEditorProps {
1616
collaboratorId: string;
1717
}
1818

19+
function usePrevious<T>(value: T): T | undefined {
20+
const ref = useRef<T>();
21+
useEffect(() => {
22+
ref.current = value;
23+
}, [value]);
24+
return ref.current;
25+
}
26+
1927
const CodeEditor: React.FC<CodeEditorProps> = ({
2028
questionId,
2129
roomId,
@@ -26,6 +34,10 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
2634
const [theme, setTheme] = useState("vs-light");
2735
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
2836
const containerRef = useRef<HTMLDivElement>(null);
37+
const [isModalVisible, setIsModalVisible] = useState(false);
38+
const [leftUser, setLeftUser] = useState<string | null>(null);
39+
40+
const prevConnectedUsers = usePrevious(connectedUsers);
2941

3042
useEffect(() => {
3143
let resizeObserver: ResizeObserver | null = null;
@@ -53,6 +65,21 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
5365
};
5466
}, []);
5567

68+
useEffect(() => {
69+
if (prevConnectedUsers) {
70+
const leftUsers = prevConnectedUsers.filter(user => !connectedUsers.includes(user));
71+
if (leftUsers.length > 0) {
72+
setLeftUser(leftUsers[0]);
73+
setIsModalVisible(true);
74+
}
75+
}
76+
}, [connectedUsers, prevConnectedUsers]);
77+
78+
const handleOk = () => {
79+
setIsModalVisible(false);
80+
setLeftUser(null);
81+
};
82+
5683
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
5784
editorRef.current = editor;
5885
onEditorIsMounted(editor);
@@ -65,6 +92,24 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
6592

6693
return (
6794
<div className={styles.container} ref={containerRef}>
95+
<Modal
96+
title="User Disconnected"
97+
open={isModalVisible}
98+
onOk={handleOk}
99+
onCancel={handleOk}
100+
footer={[
101+
<Button key="ok" type="primary" onClick={handleOk}>
102+
OK
103+
</Button>,
104+
]}
105+
>
106+
{leftUser ? (
107+
<p>User <strong>{leftUser}</strong> has left the session.</p>
108+
) : (
109+
<p>A user has left the session.</p>
110+
)}
111+
</Modal>
112+
68113
<div className={styles.toolbar}>
69114
<div className={styles.toolbarLeft}>
70115
<LanguageSelector />

frontend/src/presentation/components/CodeEditor/LeaveButton.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from "react";
2-
import { Button, Modal } from "antd";
2+
import { Button, Modal, message } from "antd";
33
import { StopOutlined } from "@ant-design/icons";
44
import styles from "./LeaveButton.module.css";
55
import { useNavigate } from "react-router-dom";
@@ -30,21 +30,38 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({
3030
setIsModalVisible(true);
3131
};
3232

33-
const handleOk = async () => {
34-
await historyUseCases.createOrUpdateUserHistory(
35-
questionId,
36-
roomId,
37-
attemptStartedAt.getTime().toString(),
38-
Date.now().toString(),
39-
collaboratorId,
40-
getEditorText(),
41-
);
42-
navigate('/')
33+
const handleCancel = () => {
4334
setIsModalVisible(false);
4435
};
4536

46-
const handleCancel = () => {
37+
const handleLeaveWithoutSaving = () => {
4738
setIsModalVisible(false);
39+
navigate('/');
40+
};
41+
42+
const handleSaveAndLeave = async () => {
43+
try {
44+
await historyUseCases.createOrUpdateUserHistory(
45+
questionId,
46+
roomId,
47+
attemptStartedAt.getTime().toString(),
48+
Date.now().toString(),
49+
collaboratorId,
50+
getEditorText(),
51+
);
52+
message.success("Your work has been saved successfully.");
53+
navigate('/');
54+
} catch (error) {
55+
if (error instanceof Error) {
56+
console.error("Failed to save before leaving:", error.message);
57+
message.error(`Failed to save before leaving: ${error.message}`);
58+
} else {
59+
console.error("Unknown error occurred during saving");
60+
message.error("Unknown error occurred during saving");
61+
}
62+
} finally {
63+
setIsModalVisible(false);
64+
}
4865
};
4966

5067
return (
@@ -58,16 +75,23 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({
5875
Leave
5976
</Button>
6077
<Modal
61-
title="Editor Content"
78+
title="Confirm Leave"
6279
open={isModalVisible}
63-
onOk={handleOk}
6480
onCancel={handleCancel}
6581
footer={[
66-
<Button key="ok" type="primary" onClick={handleOk}>
67-
OK
82+
<Button key="cancel" onClick={handleCancel} color='danger'>
83+
Cancel
84+
</Button>,
85+
<Button key="leaveWithoutSaving" onClick={handleLeaveWithoutSaving} color='danger'>
86+
Leave without saving
87+
</Button>,
88+
<Button key="saveAndLeave" type="primary" onClick={handleSaveAndLeave}>
89+
Save and Leave
6890
</Button>,
6991
]}
92+
destroyOnClose
7093
>
94+
<p>Do you want to save your code before leaving?</p>
7195
<pre className={styles.modalContent}>{editorContent}</pre>
7296
</Modal>
7397
</>
Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import React, { useState } from "react";
2-
import { Button, Modal } from "antd";
3-
import { CloudUploadOutlined } from "@ant-design/icons";
2+
import { Button, Modal, message } from "antd";
3+
import { SaveOutlined } from "@ant-design/icons";
44
import styles from "./CodeActionButtons.module.css";
55
import { historyUseCases } from "domain/usecases/HistoryUseCases";
66

7-
interface SubmitButtonProps {
7+
interface SaveButtonProps {
88
getEditorText: () => string;
99
questionId: string;
1010
roomId: string;
1111
attemptStartedAt: Date;
1212
collaboratorId: string;
1313
}
1414

15-
const SubmitButton: React.FC<SubmitButtonProps> = ({
15+
const SaveButton: React.FC<SaveButtonProps> = ({
1616
getEditorText,
1717
questionId,
1818
roomId,
@@ -28,47 +28,62 @@ const SubmitButton: React.FC<SubmitButtonProps> = ({
2828
setIsModalVisible(true);
2929
};
3030

31-
const handleOk = async () => {
32-
await historyUseCases.createOrUpdateUserHistory(
33-
questionId,
34-
roomId,
35-
attemptStartedAt.getTime().toString(),
36-
Date.now().toString(),
37-
collaboratorId,
38-
getEditorText(),
39-
);
31+
const handleCancel = () => {
4032
setIsModalVisible(false);
4133
};
4234

43-
const handleCancel = () => {
44-
setIsModalVisible(false);
35+
const handleSave = async () => {
36+
try {
37+
await historyUseCases.createOrUpdateUserHistory(
38+
questionId,
39+
roomId,
40+
attemptStartedAt.getTime().toString(),
41+
Date.now().toString(),
42+
collaboratorId,
43+
getEditorText(),
44+
);
45+
message.success("Your work has been saved successfully.");
46+
setIsModalVisible(false);
47+
} catch (error) {
48+
if (error instanceof Error) {
49+
console.error("Failed to save:", error.message);
50+
message.error(`Failed to save: ${error.message}`);
51+
} else {
52+
console.error("Unknown error occurred during saving");
53+
message.error("Unknown error occurred during saving");
54+
}
55+
}
4556
};
4657

4758
return (
4859
<>
4960
<Button
5061
onClick={showModal}
5162
type="text"
52-
icon={<CloudUploadOutlined />}
63+
icon={<SaveOutlined />}
5364
className={styles.submitButton}
5465
>
55-
Submit
66+
Save
5667
</Button>
5768
<Modal
58-
title="Editor Content"
69+
title="Confirm Save"
5970
open={isModalVisible}
60-
onOk={handleOk}
6171
onCancel={handleCancel}
6272
footer={[
63-
<Button key="ok" type="primary" onClick={handleOk}>
64-
OK
73+
<Button key="cancel" onClick={handleCancel} color='danger'>
74+
Cancel
75+
</Button>,
76+
<Button key="save" type="primary" onClick={handleSave}>
77+
Save
6578
</Button>,
6679
]}
80+
destroyOnClose
6781
>
82+
<p>Do you want to save your current work?</p>
6883
<pre className={styles.modalContent}>{editorContent}</pre>
6984
</Modal>
7085
</>
7186
);
7287
};
7388

74-
export default SubmitButton;
89+
export default SaveButton;

0 commit comments

Comments
 (0)