Skip to content

Commit 5f284c8

Browse files
authored
Merge branch 'staging' into ben/misc-collab
2 parents 5be7f3c + ba383ad commit 5f284c8

File tree

26 files changed

+1255
-79
lines changed

26 files changed

+1255
-79
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ jobs:
2929
QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }}
3030
USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }}
3131
MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }}
32+
HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }}
3233
JWT_SECRET: ${{ secrets.JWT_SECRET }}
33-
FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
34+
QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
35+
HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }}
3436
DB_CLOUD_URI: ${{ secrets.USER_SERVICE_DB_CLOUD_URI }}
3537
USER_SERVICE_PORT: ${{ vars.USER_SERVICE_PORT }}
3638
MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }}
39+
HISTORY_SERVICE_PORT: ${{ vars.HISTORY_SERVICE_PORT }}
3740
MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }}
3841
REDIS_URL: ${{ vars.REDIS_URL }}
3942
QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }}
@@ -42,9 +45,10 @@ jobs:
4245
echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env
4346
echo "NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL" >> .env
4447
echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env
48+
echo "NEXT_PUBLIC_HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env
4549
4650
cd ../question-service
47-
echo "FIREBASE_CREDENTIAL_PATH=$FIREBASE_CREDENTIAL_PATH" >> .env
51+
echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env
4852
echo "JWT_SECRET=$JWT_SECRET" >> .env
4953
5054
cd ../user-service
@@ -59,13 +63,22 @@ jobs:
5963
echo "REDIS_URL=$REDIS_URL" >> .env
6064
echo "QUESTION_SERVICE_GRPC_URL=$QUESTION_SERVICE_GRPC_URL" >> .env
6165
66+
cd ../history-service
67+
echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env
68+
echo "PORT=$HISTORY_SERVICE_PORT" >> .env
69+
6270
- name: Create Database Credential Files
6371
env:
64-
FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }}
65-
FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
72+
QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }}
73+
QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
74+
HISTORY_FIREBASE_JSON: ${{ secrets.HISTORY_SERVICE_FIREBASE_CREDENTIAL }}
75+
HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }}
6676
run: |
6777
cd ./apps/question-service
68-
echo "$FIREBASE_JSON" > "./$FIREBASE_CREDENTIAL_PATH"
78+
echo "$QUESTION_FIREBASE_JSON" > "./$QUESTION_FIREBASE_CREDENTIAL_PATH"
79+
80+
cd ../history-service
81+
echo "$HISTORY_FIREBASE_JSON" > "./$HISTORY_FIREBASE_CREDENTIAL_PATH"
6982
7083
- name: Build and Run Services
7184
run: |
@@ -87,13 +100,16 @@ jobs:
87100
USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }}
88101
QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }}
89102
MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }}
103+
HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }}
90104
run: |
91105
echo "Testing Question Service..."
92106
curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up"
93107
echo "Testing User Service..."
94108
curl -fsSL -o /dev/null $USER_SERVICE_URL && echo "User Service is up"
95109
echo "Testing Frontend..."
96110
curl -fsSL -o /dev/null $FRONTEND_URL && echo "Frontend is up"
111+
echo "Testing History Service..."
112+
curl -fsSL -o /dev/null $HISTORY_SERVICE_URL && echo "History Service is up"
97113
echo "Testing Matching Service..."
98114
if ! (echo "Hello" | websocat $MATCHING_SERVICE_URL); then
99115
echo "WebSocket for Matching Service is not live"

apps/docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ services:
5454
depends_on:
5555
- redis
5656

57+
history-service:
58+
build:
59+
context: ./history-service
60+
dockerfile: Dockerfile
61+
ports:
62+
- 8082:8082
63+
env_file:
64+
- ./history-service/.env
65+
networks:
66+
- apps_network
67+
volumes:
68+
- ./history-service:/history-service
69+
5770
redis:
5871
image: redis:latest
5972
networks:

apps/frontend/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/"
33
NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/"
44
NEXT_PUBLIC_MATCHING_SERVICE_URL="ws://localhost:8081/match"
5-
NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/"
5+
NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/"
6+
NEXT_PUBLIC_HISTORY_SERVICE_URL="http://localhost:8082/"

apps/frontend/src/app/collaboration/[id]/page.tsx

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Input,
77
Layout,
88
Modal,
9+
message,
910
Row,
1011
Select,
1112
Tabs,
@@ -31,24 +32,33 @@ import { ProgrammingLanguageOptions } from "@/utils/SelectOptions";
3132
import CollaborativeEditor, {
3233
CollaborativeEditorHandle,
3334
} from "@/components/CollaborativeEditor/CollaborativeEditor";
35+
import { CreateOrUpdateHistory } from "@/app/services/history";
36+
import { Language } from "@codemirror/language";
37+
import { WebrtcProvider } from "y-webrtc";
3438

3539
interface CollaborationProps {}
3640

3741
export default function CollaborationPage(props: CollaborationProps) {
3842
const router = useRouter();
43+
// const providerRef = useRef<WebrtcProvider | null>(null);
3944

4045
const editorRef = useRef<CollaborativeEditorHandle>(null);
4146

4247
const [isLoading, setIsLoading] = useState<boolean>(false);
4348

4449
// Code Editor States
50+
const [historyDocRefId, setHistoryDocRefId] = useState<string | undefined>(
51+
undefined
52+
);
53+
const [code, setCode] = useState<string>("");
4554
const [questionTitle, setQuestionTitle] = useState<string | undefined>(
4655
undefined
4756
);
57+
const [questionDocRefId, setQuestionDocRefId] = useState<string | undefined>(undefined);
4858
const [complexity, setComplexity] = useState<string | undefined>(undefined);
4959
const [categories, setCategories] = useState<string[]>([]); // Store the selected filter categories
5060
const [description, setDescription] = useState<string | undefined>(undefined);
51-
const [selectedLanguage, setSelectedLanguage] = useState("javascript"); // State to hold the selected language item
61+
const [selectedLanguage, setSelectedLanguage] = useState("Javascript"); // State to hold the selected language item
5262

5363
// Session states
5464
const [collaborationId, setCollaborationId] = useState<string | undefined>(
@@ -61,6 +71,7 @@ export default function CollaborationPage(props: CollaborationProps) {
6171
return storedTime ? parseInt(storedTime) : 0;
6272
}); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future)
6373
const stopwatchRef = useRef<NodeJS.Timeout | null>(null);
74+
const [matchedTopics, setMatchedTopics] = useState<string[] | undefined>(undefined);
6475

6576
// Chat states
6677
const [messageToSend, setMessageToSend] = useState<string | undefined>(
@@ -116,25 +127,68 @@ export default function CollaborationPage(props: CollaborationProps) {
116127
);
117128
};
118129

130+
// Message
131+
const [messageApi, contextHolder] = message.useMessage();
132+
133+
const successMessage = (message: string) => {
134+
messageApi.open({
135+
type: "success",
136+
content: message,
137+
});
138+
};
139+
140+
const sendCodeSavedStatusToMatchedUser = () => {
141+
if (!providerRef.current) {
142+
throw new Error("Provider not initialized");
143+
}
144+
providerRef.current.awareness.setLocalStateField("codeSavedStatus", true);
145+
}
146+
147+
const handleSubmitCode = async () => {
148+
if (!collaborationId) {
149+
throw new Error("Collaboration ID not found");
150+
}
151+
const data = await CreateOrUpdateHistory({
152+
title: questionTitle ?? "",
153+
code: code,
154+
language: selectedLanguage,
155+
user: currentUser ?? "",
156+
matchedUser: matchedUser ?? "",
157+
matchId: collaborationId ?? "",
158+
matchedTopics: matchedTopics ?? [],
159+
questionDocRefId: questionDocRefId ?? "",
160+
questionDifficulty: complexity ?? "",
161+
questionTopics: categories,
162+
}, collaborationId);
163+
successMessage("Code saved successfully!");
164+
sendCodeSavedStatusToMatchedUser();
165+
}
166+
167+
const handleCodeChange = (code: string) => {
168+
setCode(code);
169+
}
170+
119171
// Fetch the question on initialisation
120172
useEffect(() => {
121173
if (!isLoading) {
122174
setIsLoading(true);
123175
}
124176

125177
// Retrieve details from localstorage
126-
const docRefId: string = localStorage.getItem("docRefId") ?? "";
178+
const questionDocRefId: string = localStorage.getItem("questionDocRefId") ?? "";
127179
const collabId: string = localStorage.getItem("collabId") ?? "";
128180
const matchedUser: string = localStorage.getItem("matchedUser") ?? "";
129181
const currentUser: string = localStorage.getItem("user") ?? "";
182+
const matchedTopics: string[] = localStorage.getItem("matchedTopics")?.split(",") ?? [];
130183

131184
// Set states from localstorage
132185
setCollaborationId(collabId);
133186
setMatchedUser(matchedUser);
134187
setCurrentUser(currentUser);
188+
setMatchedTopics(matchedTopics);
189+
setQuestionDocRefId(questionDocRefId);
135190

136-
// Fetch question and set question states
137-
GetSingleQuestion(docRefId).then((data: Question) => {
191+
GetSingleQuestion(questionDocRefId).then((data: Question) => {
138192
setQuestionTitle(`${data.id}. ${data.title}`);
139193
setComplexity(data.complexity);
140194
setCategories(data.categories);
@@ -202,13 +256,18 @@ export default function CollaborationPage(props: CollaborationProps) {
202256
// Remove localstorage variables for collaboration
203257
localStorage.removeItem("session-duration"); // TODO: Remove this after collaboration backend data stored
204258
localStorage.removeItem("user");
205-
localStorage.removeItem("collabId");
206-
localStorage.removeItem("docRefId");
207259
localStorage.removeItem("matchedUser");
260+
localStorage.removeItem("collabId");
261+
localStorage.removeItem("questionDocRefId");
262+
localStorage.removeItem("matchedTopics");
263+
264+
// Redirect back to matching page
265+
router.push("/matching");
208266
};
209267

210268
return (
211269
<Layout className="collaboration-layout">
270+
{contextHolder}
212271
<Header selectedKey={undefined} />
213272
<Content className="collaboration-content">
214273
<Modal
@@ -318,10 +377,10 @@ export default function CollaborationPage(props: CollaborationProps) {
318377
Code
319378
</div>
320379
{/* TODO: Link to execution service for code submission */}
321-
<Button
322-
icon={<SendOutlined />}
323-
iconPosition="end"
324-
className="code-submit-button"
380+
<Button
381+
icon={<SendOutlined />}
382+
iconPosition="end"
383+
onClick={() => handleSubmitCode()}
325384
>
326385
Submit
327386
</Button>
@@ -334,6 +393,9 @@ export default function CollaborationPage(props: CollaborationProps) {
334393
language={selectedLanguage}
335394
setMatchedUser={setMatchedUser}
336395
handleCloseCollaboration={handleCloseCollaboration}
396+
// providerRef={providerRef}
397+
matchedUser={matchedUser}
398+
onCodeChange={handleCodeChange}
337399
/>
338400
)}
339401
</div>

apps/frontend/src/app/matching/MatchingModal.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ const MatchingModal: React.FC<MatchingModalProps> = ({
5959
cancel={() => {
6060
setClosedType("cancelled");
6161
}}
62-
name1={matchingState.info?.myName || ""}
63-
name2={matchingState.info?.partnerName || ""}
62+
name1={matchingState.info?.user || ""}
63+
name2={matchingState.info?.matchedUser || ""}
6464
/>
6565
);
6666
}
@@ -97,18 +97,20 @@ const MatchingModal: React.FC<MatchingModalProps> = ({
9797
join={() => {
9898
matchingState.ok();
9999
setClosedType("joined");
100-
localStorage.setItem("user", matchingState.info.myName);
101-
localStorage.setItem("collabId", matchingState.info.matchId);
102-
localStorage.setItem("docRefId", matchingState.info.docRefId);
100+
localStorage.setItem("user", matchingState.info.user);
103101
localStorage.setItem(
104102
"matchedUser",
105-
matchingState.info.partnerName
103+
matchingState.info.matchedUser
106104
);
105+
localStorage.setItem("collabId", matchingState.info.matchId);
106+
localStorage.setItem("questionDocRefId", matchingState.info.questionDocRefId);
107+
localStorage.setItem("matchedTopics", matchingState.info.matchedTopics.join(","));
108+
107109
// Redirect to collaboration page
108110
router.push(`/collaboration/${matchingState.info.matchId}`);
109111
}}
110-
name1={matchingState.info.myName}
111-
name2={matchingState.info.partnerName}
112+
name1={matchingState.info.user}
113+
name2={matchingState.info.matchedUser}
112114
/>
113115
);
114116
case "timeout":
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const HISTORY_SERVICE_URL = process.env.NEXT_PUBLIC_HISTORY_SERVICE_URL;
2+
3+
export interface History {
4+
title: string;
5+
code: string;
6+
language: string;
7+
user: string;
8+
matchedUser: string;
9+
matchId: string;
10+
matchedTopics: string[];
11+
questionDocRefId: string;
12+
questionDifficulty: string;
13+
questionTopics: string[];
14+
createdAt?: string;
15+
updatedAt?: string;
16+
}
17+
18+
export const CreateOrUpdateHistory = async (
19+
history: History,
20+
matchId: string,
21+
): Promise<History> => {
22+
const response = await fetch(
23+
`${HISTORY_SERVICE_URL}histories/${matchId}`,
24+
{
25+
method: "PUT",
26+
headers: {
27+
"Content-Type": "application/json",
28+
},
29+
body: JSON.stringify(history),
30+
}
31+
);
32+
33+
if (response.status === 200) {
34+
return response.json();
35+
} else {
36+
throw new Error(
37+
`Error saving history: ${response.status} ${response.statusText}`
38+
);
39+
}
40+
}
41+
42+
export const GetHistory = async (
43+
matchId: string,
44+
): Promise<History> => {
45+
const response = await fetch(
46+
`${HISTORY_SERVICE_URL}histories/${matchId}`,
47+
{
48+
method: "GET",
49+
headers: {
50+
"Content-Type": "application/json",
51+
},
52+
}
53+
);
54+
55+
if (response.status === 200) {
56+
return response.json();
57+
} else {
58+
throw new Error(
59+
`Error reading history: ${response.status} ${response.statusText}`
60+
);
61+
}
62+
}
63+
64+
export const GetUserHistories = async (
65+
username: string,
66+
): Promise<History[]> => {
67+
const response = await fetch(
68+
`${HISTORY_SERVICE_URL}histories/${username}`,
69+
{
70+
method: "GET",
71+
headers: {
72+
"Content-Type": "application/json",
73+
},
74+
}
75+
);
76+
77+
if (response.status === 200) {
78+
return response.json();
79+
} else {
80+
throw new Error(
81+
`Error reading user histories: ${response.status} ${response.statusText}`
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)