Skip to content

Commit 4b51768

Browse files
committed
Merge branch 'develop' into feature/user-service
2 parents 9072365 + 4b6f986 commit 4b51768

File tree

15 files changed

+8721
-9339
lines changed

15 files changed

+8721
-9339
lines changed

frontend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"preview": "vite preview"
1313
},
1414
"dependencies": {
15+
"@codemirror/lang-cpp": "^6.0.3",
16+
"@codemirror/lang-java": "^6.0.2",
17+
"@codemirror/lang-javascript": "^6.2.4",
18+
"@codemirror/lang-python": "^6.2.1",
1519
"@tanstack/react-query": "^5.90.2",
1620
"@tanstack/react-query-devtools": "^5.90.2",
1721
"axios": "^1.12.2",

frontend/src/collaboration/components/CodeMirror.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// src/codemirror.tsx
22
import React, {useEffect, useRef} from 'react';
3-
import {EditorState} from '@codemirror/state';
3+
import {EditorState, Compartment, type Extension} from '@codemirror/state';
44
import {
55
EditorView,
66
keymap,
@@ -21,7 +21,13 @@ import {
2121
foldGutter,
2222
foldKeymap,
2323
} from '@codemirror/language';
24-
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands';
24+
25+
import {javascript} from '@codemirror/lang-javascript';
26+
import {python} from '@codemirror/lang-python';
27+
import {cpp} from '@codemirror/lang-cpp';
28+
import {java} from '@codemirror/lang-java';
29+
30+
import {defaultKeymap, history, historyKeymap, indentMore, indentLess} from '@codemirror/commands';
2531
import {searchKeymap, highlightSelectionMatches} from '@codemirror/search';
2632
import {
2733
autocompletion,
@@ -34,18 +40,32 @@ import {yCollab} from 'y-codemirror.next';
3440
import * as Y from 'yjs';
3541
import {Awareness} from 'y-protocols/awareness';
3642

43+
const languageCompartment = new Compartment();
44+
45+
const languageMap: { [key: string]: () => Extension } = {
46+
javascript: () => javascript(),
47+
python: () => python(),
48+
cpp: () => cpp(),
49+
java: () => java(),
50+
default: () => [], // plain text mode
51+
};
52+
3753
interface CodeMirrorProps {
3854
ytext: Y.Text;
3955
awareness: Awareness;
56+
languageConfig: string;
4057
}
4158

42-
export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
59+
export default function CodeMirror({ytext, awareness, languageConfig}: CodeMirrorProps) {
4360
const editorRef = useRef<HTMLDivElement>(null);
4461
const viewRef = useRef<EditorView | null>(null);
4562

4663
useEffect(() => {
4764
if (!editorRef.current || !ytext || !awareness) return;
4865

66+
const lazyInitialLang = languageMap[languageConfig.toLowerCase()] || languageMap.default;
67+
const initialLanguageExtension = lazyInitialLang();
68+
4969
// Create the editor view with yCollab extension
5070
const view = new EditorView({
5171
doc: ytext.toString(),
@@ -68,7 +88,9 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
6888
// Re-indent lines when typing specific input
6989
indentOnInput(),
7090
// Highlight syntax with a default style
71-
syntaxHighlighting(defaultHighlightStyle),
91+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
92+
// Dyanmic configuration for language mode
93+
languageCompartment.of(initialLanguageExtension),
7294
// Highlight matching brackets near cursor
7395
bracketMatching(),
7496
// Automatically close brackets
@@ -88,6 +110,17 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
88110
// Yjs collaboration extension for CodeMirror 6
89111
yCollab(ytext, awareness),
90112
keymap.of([
113+
{
114+
key: "Tab",
115+
preventDefault: true,
116+
run: indentMore,
117+
},
118+
{
119+
key: "Shift-Tab",
120+
preventDefault: true,
121+
run: indentLess,
122+
},
123+
91124
// Closed-brackets aware backspace
92125
...closeBracketsKeymap,
93126
// A large set of basic bindings
@@ -117,6 +150,20 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
117150
};
118151
}, [ytext, awareness]);
119152

153+
// Effect to handle language changes
154+
useEffect(() => {
155+
const view = viewRef.current;
156+
if (!view) return;
157+
158+
const newLazyLangFn = languageMap[languageConfig.toLowerCase()] || languageMap.default;
159+
const newExtension = newLazyLangFn();
160+
161+
// Reconfigures compartment with the new language extension
162+
view.dispatch({
163+
effects: languageCompartment.reconfigure(newExtension),
164+
});
165+
}, [languageConfig]);
166+
120167
return (
121168
<div
122169
ref={editorRef}
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
import useCollabEditor from '../hooks/useCollabEditor';
22
import CodeMirror from './CodeMirror';
3+
import React from 'react';
4+
5+
const LANGUAGE_OPTIONS = [
6+
{ value: 'python', label: 'Python' },
7+
{ value: 'javascript', label: 'JavaScript' },
8+
{ value: 'cpp', label: 'C++' },
9+
{ value: 'java', label: 'Java' },
10+
{ value: 'default', label: 'Plain Text' },
11+
];
312

413
export default function CollabEditor({roomId}: {roomId: string}) {
5-
const {ytext, awareness, isReady} = useCollabEditor({roomId});
14+
const {ytext, awareness, isReady, languageConfig, setSharedLanguage} = useCollabEditor({roomId});
615
if (!isReady || !ytext) {
716
return <div>Loading...</div>;
817
}
918

19+
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
20+
setSharedLanguage(event.target.value);
21+
};
22+
1023
return (
1124
<div style={{padding: '20px'}}>
12-
<h2>Happy Coding :D</h2>
13-
<CodeMirror ytext={ytext} awareness={awareness} />
25+
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px'}}>
26+
<h2>Happy Coding :D</h2>
27+
<label>
28+
Language:
29+
<select
30+
value={languageConfig}
31+
onChange={handleLanguageChange}
32+
style={{marginLeft: '10px', padding: '5px', borderRadius: '4px', border: '1px solid #ccc'}}
33+
>
34+
{LANGUAGE_OPTIONS.map(option => (
35+
<option key={option.value} value={option.value}>
36+
{option.label}
37+
</option>
38+
))}
39+
</select>
40+
</label>
41+
</div>
42+
<CodeMirror ytext={ytext} awareness={awareness} languageConfig={languageConfig} />
1443
</div>
1544
);
1645
}

frontend/src/collaboration/hooks/useCollabEditor.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// src/codemirroreditor.tsx
2-
import React, {useEffect, useRef, useState} from 'react';
2+
import React, {useEffect, useRef, useState, useCallback} from 'react';
33
import YPartyKitProvider from 'y-partykit/provider';
44
import * as Y from 'yjs';
55
import toast from 'react-hot-toast';
@@ -92,15 +92,32 @@ const handleUsersRemoved = (
9292
});
9393
};
9494

95+
interface CollabEditorHook {
96+
provider: YPartyKitProvider | null;
97+
ytext: Y.Text | null;
98+
awareness: any;
99+
isReady: boolean;
100+
languageConfig: string; // current selected language
101+
setSharedLanguage: (newLang: string) => void;
102+
}
95103

96-
export default function useCollabEditor({roomId}: {roomId: string}) {
104+
export default function useCollabEditor({roomId}: {roomId: string}): CollabEditorHook {
97105
const [isReady, setIsReady] = useState<boolean>(false);
106+
const [languageConfig, setLanguageConfig] = useState<string>('python'); // Default to Python
107+
const configMapRef = useRef<Y.Map<string> | null>(null);
98108
const ytextRef = useRef<Y.Text | null>(null);
99109
const providerRef = useRef<YPartyKitProvider | null>(null);
100110
const awarenessRef = useRef<any>(null);
101111
const isFirstChangeRef = useRef<boolean>(true);
102112
const userNamesRef = useRef<Map<number, string>>(new Map());
103113
const recentlyRemovedRef = useRef<Set<number>>(new Set());
114+
115+
const setSharedLanguage = useCallback((newLang: string) => {
116+
if (configMapRef.current) {
117+
configMapRef.current.set('language', newLang);
118+
}
119+
}, []);
120+
104121
const timeoutRefs = useRef(new Map<number, NodeJS.Timeout>());
105122

106123
useEffect(() => {
@@ -124,6 +141,23 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
124141
const ytext = provider.doc.getText('codemirror');
125142
ytextRef.current = ytext;
126143

144+
const configMap = provider.doc.getMap<string>('config');
145+
configMapRef.current = configMap;
146+
147+
if (!configMap.get('language')) {
148+
console.log('Setting default language: python');
149+
configMap.set('language', 'python');
150+
}
151+
setLanguageConfig(configMap.get('language') || 'python');
152+
153+
const configMapHandler = () => {
154+
const newLang = configMap.get('language') || 'python';
155+
setLanguageConfig(newLang);
156+
console.log('Shared language updated to:', newLang);
157+
};
158+
159+
configMap.observe(configMapHandler);
160+
127161
// Sets up user awareness
128162
const currUserId = provider.awareness.clientID;
129163
const username = getRandomName();
@@ -180,6 +214,7 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
180214
return () => {
181215
console.log('Cleanup! destroying provider for room:', roomId);
182216

217+
configMap.unobserve(configMapHandler);
183218
provider.awareness.off('change', awarenessChangeHandler);
184219
userNamesRef.current.clear();
185220
recentlyRemovedRef.current.clear();
@@ -197,13 +232,16 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
197232
providerRef.current = null;
198233
ytextRef.current = null;
199234
awarenessRef.current = null;
235+
configMapRef.current = null;
200236
};
201-
}, [roomId]);
237+
}, [roomId, setSharedLanguage]);
202238

203239
return {
204240
provider: providerRef.current,
205241
ytext: ytextRef.current,
206242
awareness: awarenessRef.current,
207243
isReady,
244+
languageConfig,
245+
setSharedLanguage,
208246
};
209247
}

matching-service/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "index.js",
66
"type": "module",
77
"scripts": {
8-
"dev": "node ./src/index.js",
8+
"dev": "tsx watch ./src/index.ts",
99
"lint": "cd .. && npm run lint:service matching-service --ext .ts",
1010
"lint:fix": "cd .. && npm run lint:service matching-service --ext .ts --fix",
1111
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
@@ -15,10 +15,15 @@
1515
"author": "",
1616
"license": "ISC",
1717
"dependencies": {
18-
"express": "^5.1.0"
18+
"cors": "^2.8.5",
19+
"dotenv": "^17.2.2",
20+
"express": "^5.1.0",
21+
"tsx": "^4.20.6",
22+
"ws": "^8.18.3"
1923
},
2024
"devDependencies": {
2125
"@types/express": "^5.0.3",
22-
"@types/node": "^24.3.1"
26+
"@types/node": "^24.3.1",
27+
"@types/ws": "^8.18.1"
2328
}
2429
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import dotenv from 'dotenv';
2+
import path from 'path';
3+
import {fileURLToPath} from 'url';
4+
5+
// Solution adapted from:
6+
// https://stackoverflow.com/questions/64383909/dirname-is-not-defined-error-in-node-js-14-version
7+
const filename = fileURLToPath(import.meta.url);
8+
const dirname = path.dirname(filename);
9+
10+
dotenv.config({path: path.resolve(dirname, '../../.env')});
11+
12+
/**
13+
* Processes all environment variables.
14+
*
15+
* @param {string} key - The name of the environment variable to retrieve.
16+
* @param {string} [defaultValue] - Optional fallback value if no environment variable is provided.
17+
* @returns {string} Environment variable value.
18+
* @throws {Error} If the environment variable is missing and no default value is provided.
19+
*/
20+
const getEnv = (key: string, defaultValue?: string): string => {
21+
const value = process.env[key] || defaultValue;
22+
23+
if (value === undefined) {
24+
throw new Error(`${key} environment variable is missing!`);
25+
}
26+
27+
return value;
28+
};
29+
30+
export const NODE_ENV = getEnv('NODE_ENV', 'development');
31+
export const MATCHING_SERVICE_PORT = getEnv('MATCHING_SERVICE_PORT', '8081');
32+
export const APP_ORIGIN = getEnv('APP_ORIGIN');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Constants for all HTTP response status codes
3+
* Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status
4+
*/
5+
6+
export const HTTP_OK = 200;
7+
export const HTTP_CREATED = 201;
8+
export const HTTP_ACCEPTED = 202;
9+
export const HTTP_BAD_REQUEST = 400;
10+
export const HTTP_INTERNAL_SERVER_ERROR = 500;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Request, Response } from 'express';
2+
import { findOrQueueUser } from '../services/matchingService';
3+
import type { MatchRequest } from '../models/matchModel';
4+
import { HTTP_OK, HTTP_ACCEPTED, HTTP_BAD_REQUEST} from '../constants/httpStatus';
5+
6+
export const handleMatchRequest = (req: Request, res: Response) => {
7+
const matchRequestData: MatchRequest = req.body;
8+
9+
const atLeastOneDifficulty: boolean = matchRequestData.criteria.difficulties && matchRequestData.criteria.difficulties.length > 0;
10+
const atLeastOneTopic: boolean = matchRequestData.criteria.topics && matchRequestData.criteria.topics.length > 0;
11+
12+
// validate input
13+
if (!atLeastOneDifficulty && !atLeastOneTopic) {
14+
return res.status(HTTP_BAD_REQUEST).json({ message: "Difficulty and at least one topic are required." });
15+
}
16+
17+
// call the service to perform the matching logic
18+
const result = findOrQueueUser(matchRequestData.userId, matchRequestData.criteria);
19+
20+
// send the appropriate HTTP response based on the service's result
21+
if (result.status === 'matched') {
22+
return res.status(HTTP_OK).json(result);
23+
} else {
24+
return res.status(HTTP_ACCEPTED).json(result); // in the queue waiting for a match
25+
}
26+
};
27+

matching-service/src/index.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)