Skip to content

Commit dfa2313

Browse files
committed
Resolve merge conflicts
2 parents dde268d + 7c8c292 commit dfa2313

22 files changed

+1321
-27
lines changed

frontend/package-lock.json

Lines changed: 644 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"@monaco-editor/react": "^4.6.0",
67
"@testing-library/jest-dom": "^5.17.0",
78
"@testing-library/react": "^13.4.0",
89
"@testing-library/user-event": "^13.5.0",
@@ -13,8 +14,10 @@
1314
"@uiw/react-md-editor": "^4.0.4",
1415
"antd": "^5.20.6",
1516
"highlight.js": "^11.10.0",
17+
"monaco-editor": "^0.52.0",
1618
"react": "^18.3.1",
1719
"react-dom": "^18.3.1",
20+
"react-resizable-layout": "^0.7.2",
1821
"react-router-dom": "^6.26.2",
1922
"react-scripts": "5.0.1",
2023
"react-syntax-highlighter": "^15.5.0",
@@ -24,7 +27,11 @@
2427
"remark-gfm": "^4.0.0",
2528
"socket.io-client": "^4.8.0",
2629
"typescript": "^4.9.5",
27-
"web-vitals": "^2.1.4"
30+
"web-vitals": "^2.1.4",
31+
"y-monaco": "^0.1.6",
32+
"y-webrtc": "^10.3.0",
33+
"y-websocket": "^2.0.4",
34+
"yjs": "^13.6.20"
2835
},
2936
"scripts": {
3037
"start": "react-scripts start",
@@ -53,6 +60,7 @@
5360
"devDependencies": {
5461
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
5562
"@types/axios": "^0.14.0",
63+
"@types/react-resizable": "^3.0.8",
5664
"@types/react-syntax-highlighter": "^15.5.13",
5765
"@types/socket.io-client": "^3.0.0"
5866
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import axios from "axios";
2+
import { CodeExecResult } from "domain/entities/CodeExecResult";
3+
import { Language } from "domain/entities/Language";
4+
5+
const API_URL = "https://emkc.org/api/v2/piston";
6+
7+
const pistonAxios = axios.create({ baseURL: API_URL });
8+
9+
class PistonClient {
10+
static async executeCode(language: Language, sourceCode: string): Promise<CodeExecResult> {
11+
const res = await pistonAxios.post("/execute", {
12+
language: language.language,
13+
version: language.version,
14+
files: [{ content: sourceCode }]
15+
});
16+
const output = res.data.run;
17+
return {
18+
stdout: output.stdout,
19+
stderr: output.stderr,
20+
success: output.code === 0,
21+
timeout: output.signal !== null
22+
};
23+
}
24+
25+
static async getLanguageVersions() {
26+
const res = await pistonAxios.get("/runtimes");
27+
return res.data;
28+
}
29+
}
30+
31+
export default PistonClient;

frontend/src/data/repositories/mockQuestionRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { mockCategoryRemoteDataSource } from "./mockCategoryRepository";
44

55
const { categories } = mockCategoryRemoteDataSource;
66

7-
const initialQuestions: Question[] = [
7+
export const initialQuestions: Question[] = [
88
{
99
_id: "i1",
1010
code: "1",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { createContext, useState, useContext, ReactNode, useEffect, useMemo } from "react";
2+
import * as Y from "yjs";
3+
import * as monaco from "monaco-editor";
4+
import { MonacoBinding } from "y-monaco";
5+
import { WebsocketProvider } from "y-websocket";
6+
import { useAuth } from "./AuthContext";
7+
import { toast } from "react-toastify";
8+
import { COLLABORATION_AWARENESS_KEYS, COLLABORATION_YMAP_KEYS } from "presentation/utils/constants";
9+
import PistonClient from "data/piston/PistonClient";
10+
import { Language } from "domain/entities/Language";
11+
import { CodeExecResult } from "domain/entities/CodeExecResult";
12+
13+
interface CollaborationContextType {
14+
onEditorIsMounted: (editor: monaco.editor.IStandaloneCodeEditor) => void;
15+
selectedLanguage: Language;
16+
languages: Language[];
17+
handleChangeLanguage: (lang: Language) => void;
18+
handleExecuteCode: () => Promise<void>;
19+
isExecuting: boolean;
20+
execResult: CodeExecResult | null;
21+
setRoomId: (roomId: string) => void;
22+
connectedUsers: string[];
23+
}
24+
25+
const CollaborationContext = createContext<CollaborationContextType | undefined>(undefined);
26+
27+
export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
28+
const { user } = useAuth();
29+
const username = user?.username;
30+
31+
const { USERNAME } = COLLABORATION_AWARENESS_KEYS;
32+
const { SELECTED_LANGUAGE } = COLLABORATION_YMAP_KEYS;
33+
34+
const [selectedLanguage, setSelectedLanguage] = useState<Language>({
35+
language: "javascript",
36+
version: "1.32.3",
37+
alias: "Javascript"
38+
});
39+
40+
const ydoc = useMemo(() => new Y.Doc(), []);
41+
const ymap: Y.Map<any> = useMemo(() => ydoc.getMap("sharedMap"), [ydoc]);
42+
43+
const [roomId, setRoomId] = useState<string | null>(null);
44+
45+
const [languages, setLanguages] = useState<Language[]>([]);
46+
const [execResult, setExecResult] = useState<CodeExecResult | null>(null);
47+
const [isExecuting, setIsExecuting] = useState<boolean>(false);
48+
const [connectedUsers, setConnectedUsers] = useState<string[]>([]);
49+
50+
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
51+
const [provider, setProvider] = useState<WebsocketProvider | null>(null);
52+
const [binding, setBinding] = useState<MonacoBinding | null>(null);
53+
54+
// This effect manages the lifetime of the yjs doc and the provider
55+
useEffect(() => {
56+
if (roomId == null) {
57+
return;
58+
}
59+
const provider = new WebsocketProvider("ws://localhost:1234", roomId, ydoc);
60+
setProvider(provider);
61+
62+
provider.awareness.setLocalStateField(USERNAME, username);
63+
provider.awareness.on("change", (update: any) => {
64+
const users = Array.from(provider.awareness.getStates().values());
65+
setConnectedUsers(users.map((user) => user[USERNAME]));
66+
// TODO: Some UI feedback about connection status of the other user
67+
});
68+
69+
return () => {
70+
provider?.destroy();
71+
ydoc?.destroy();
72+
};
73+
}, [ydoc, roomId]);
74+
75+
// This effect manages the lifetime of the editor binding
76+
useEffect(() => {
77+
if (provider == null || editor == null || editor.getModel() == null) {
78+
return;
79+
}
80+
81+
const binding = new MonacoBinding(
82+
ydoc.getText("monaco"),
83+
editor.getModel()!,
84+
new Set([editor]),
85+
provider?.awareness
86+
);
87+
88+
setBinding(binding);
89+
90+
ymap.observe((event) => {
91+
event.changes.keys.forEach((change, key) => {
92+
if (key === SELECTED_LANGUAGE) {
93+
const language: Language = ymap.get(SELECTED_LANGUAGE);
94+
setSelectedLanguage(language);
95+
const model = editor.getModel();
96+
monaco.editor.setModelLanguage(model!, language.language);
97+
}
98+
});
99+
});
100+
101+
// Set the editor's language
102+
const language: Language = ymap.get(SELECTED_LANGUAGE);
103+
const model = editor.getModel();
104+
monaco.editor.setModelLanguage(model!, language?.language ?? "javascript");
105+
106+
return () => {
107+
binding.destroy();
108+
};
109+
}, [ydoc, provider, editor, ymap]);
110+
111+
useEffect(() => {
112+
initialiseLanguages();
113+
}, []);
114+
115+
const initialiseLanguages = async () => {
116+
// Initialise language dropdown
117+
const allLanguages = monaco.languages.getLanguages();
118+
const pistonLanguageVersions = await PistonClient.getLanguageVersions();
119+
setLanguages(
120+
allLanguages
121+
.filter((lang) => pistonLanguageVersions.some((pistonLang: any) => pistonLang.language === lang.id))
122+
.map((lang) => ({
123+
alias: lang.aliases && lang.aliases.length > 0 ? lang.aliases[0] : lang.id,
124+
language: lang.id,
125+
version: pistonLanguageVersions.find((pistonLang: any) => pistonLang.language === lang.id)?.version
126+
}))
127+
);
128+
};
129+
130+
const handleChangeLanguage = (lang: Language) => {
131+
ymap.set(SELECTED_LANGUAGE, lang);
132+
};
133+
134+
const handleExecuteCode = async () => {
135+
try {
136+
setIsExecuting(true);
137+
const sourceCode = editor?.getValue();
138+
if (!sourceCode) {
139+
// TODO
140+
return;
141+
}
142+
const output: CodeExecResult = await PistonClient.executeCode(selectedLanguage, sourceCode);
143+
setExecResult(output);
144+
} catch (e) {
145+
toast.error("There was an issue running the code");
146+
} finally {
147+
setIsExecuting(false);
148+
}
149+
};
150+
151+
const onEditorIsMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
152+
setEditor(editor);
153+
};
154+
155+
return (
156+
<CollaborationContext.Provider
157+
value={{
158+
onEditorIsMounted,
159+
setRoomId,
160+
selectedLanguage,
161+
languages,
162+
handleChangeLanguage,
163+
handleExecuteCode,
164+
isExecuting,
165+
execResult,
166+
connectedUsers
167+
}}
168+
>
169+
{children}
170+
</CollaborationContext.Provider>
171+
);
172+
};
173+
174+
export const useCollaboration = () => {
175+
const context = useContext(CollaborationContext);
176+
if (!context) {
177+
throw new Error("useCollaboration must be used within a CollaborationProvider");
178+
}
179+
return context;
180+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface CodeExecResult {
2+
stdout: string;
3+
stderr: string;
4+
success: boolean;
5+
timeout: boolean;
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface Language {
2+
alias: string;
3+
language: string;
4+
version: string;
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.submitButton {
2+
background-color: #ffa500;
3+
}
4+
5+
.runButton {
6+
background-color: black;
7+
color: white;
8+
}
9+
10+
button {
11+
width: fit-content;
12+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Button } from "antd";
2+
import { useCollaboration } from "domain/context/CollaborationContext";
3+
import { PlayCircleOutlined, CloudUploadOutlined } from "@ant-design/icons";
4+
import styles from "./CodeActionButtons.module.css";
5+
6+
interface CodeActionButtonsProps {
7+
disabled?: boolean;
8+
}
9+
export const CodeActionButtons: React.FC<CodeActionButtonsProps> = ({ disabled = false }) => {
10+
const { handleExecuteCode } = useCollaboration();
11+
return (
12+
<>
13+
<Button
14+
onClick={async () => {
15+
await handleExecuteCode();
16+
}}
17+
className={styles.runButton}
18+
icon={<PlayCircleOutlined />}
19+
disabled={disabled}
20+
>
21+
Run
22+
</Button>
23+
<Button className={styles.submitButton} icon={<CloudUploadOutlined />} disabled={disabled}>
24+
Submit
25+
</Button>
26+
</>
27+
);
28+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.container {
2+
height: 100%;
3+
width: 100%;
4+
display: flex;
5+
flex-direction: column;
6+
padding: 0 8px;
7+
overflow-y: hidden;
8+
}
9+
10+
.toolbar {
11+
padding: 4px;
12+
height: 40px;
13+
display: flex;
14+
width: 100%;
15+
justify-content: space-between;
16+
align-items: center;
17+
}
18+
19+
.toolbarLeft,
20+
.toolbarRight {
21+
display: flex;
22+
justify-content: start;
23+
align-items: center;
24+
gap: 8px;
25+
}
26+
27+
.toolbarLeft,
28+
.editor {
29+
flex: 1;
30+
}
31+
32+
.buttonGroup {
33+
display: flex;
34+
flex-direction: row;
35+
justify-content: flex-end;
36+
gap: 8px;
37+
}
38+
39+
.pendingExecution {
40+
display: flex;
41+
align-items: center;
42+
gap: 8px;
43+
}
44+
45+
.pendingExecution p {
46+
padding: 0;
47+
}
48+
49+
.connectionStatusContainer {
50+
display: flex;
51+
justify-content: flex-end;
52+
align-items: center;
53+
gap: 24px;
54+
}
55+
56+
.connectionStatus {
57+
display: flex;
58+
align-items: center;
59+
gap: 6px;
60+
}
61+
62+
.statusUsername {
63+
line-height: 100%;
64+
max-width: 120px;
65+
overflow: hidden;
66+
text-overflow: ellipsis;
67+
}
68+
69+
.greenCircle {
70+
width: 10px;
71+
height: 10px;
72+
background-color: rgb(18, 221, 18);
73+
border-radius: 50%;
74+
}

0 commit comments

Comments
 (0)