Skip to content

Commit 84830a8

Browse files
committed
Redirects to question on match found and connect to websocket
1 parent 6a291d0 commit 84830a8

File tree

5 files changed

+189
-55
lines changed

5 files changed

+189
-55
lines changed

collab/main.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,30 @@ var upgrader = websocket.Upgrader{
1818
// Client represents a WebSocket client
1919
type Client struct {
2020
conn *websocket.Conn
21-
secret string
21+
roomID string
2222
}
2323

24-
// Hub maintains the set of active clients, broadcasts messages, and stores the current color per secret
24+
// Hub maintains the set of active clients, broadcasts messages, and stores the current workspace per roomID
2525
type Hub struct {
2626
clients map[*Client]bool
27-
colors map[string]string // Store current color per secret
27+
workspaces map[string]string
2828
broadcast chan Message
2929
register chan *Client
3030
unregister chan *Client
3131
mutex sync.Mutex
3232
}
3333

34-
// Message represents a message with the associated secret
34+
// Message represents a message with the associated roomID
3535
type Message struct {
36-
secret string
36+
roomID string
3737
content []byte
3838
}
3939

4040
// NewHub creates a new hub instance
4141
func NewHub() *Hub {
4242
return &Hub{
4343
clients: make(map[*Client]bool),
44-
colors: make(map[string]string),
44+
workspaces: make(map[string]string),
4545
broadcast: make(chan Message),
4646
register: make(chan *Client),
4747
unregister: make(chan *Client),
@@ -67,10 +67,10 @@ func (h *Hub) Run() {
6767

6868
case message := <-h.broadcast:
6969
h.mutex.Lock()
70-
// Update the current color for this secret
71-
h.colors[message.secret] = string(message.content)
70+
// Update the current workspace for this roomID
71+
h.workspaces[message.roomID] = string(message.content)
7272
for client := range h.clients {
73-
if client.secret == message.secret {
73+
if client.roomID == message.roomID {
7474
err := client.conn.WriteMessage(websocket.TextMessage, message.content)
7575
if err != nil {
7676
log.Printf("Error sending message: %v", err)
@@ -86,9 +86,9 @@ func (h *Hub) Run() {
8686

8787
// ServeWs handles WebSocket requests
8888
func serveWs(hub *Hub, c *gin.Context) {
89-
secret := c.Query("secret")
90-
if secret == "" {
91-
http.Error(c.Writer, "Secret required", http.StatusBadRequest)
89+
roomID := c.Query("roomID")
90+
if roomID == "" {
91+
http.Error(c.Writer, "roomID required", http.StatusBadRequest)
9292
return
9393
}
9494

@@ -98,13 +98,12 @@ func serveWs(hub *Hub, c *gin.Context) {
9898
return
9999
}
100100

101-
client := &Client{conn: conn, secret: secret}
101+
client := &Client{conn: conn, roomID: roomID}
102102
hub.register <- client
103103

104104
go handleMessages(client, hub)
105105
}
106106

107-
// HandleMessages listens for color messages from the client
108107
func handleMessages(client *Client, hub *Hub) {
109108
defer func() {
110109
hub.unregister <- client
@@ -118,31 +117,31 @@ func handleMessages(client *Client, hub *Hub) {
118117
}
119118

120119
// Broadcast the message to other clients
121-
hub.broadcast <- Message{secret: client.secret, content: message}
120+
hub.broadcast <- Message{roomID: client.roomID, content: message}
122121
}
123122
}
124123

125-
// Status endpoint that shows the number of clients and the current color for each secret
124+
// Status endpoint that shows the number of clients and the current color for each roomID
126125
func statusHandler(hub *Hub) gin.HandlerFunc {
127126
return func(c *gin.Context) {
128127
hub.mutex.Lock()
129128
defer hub.mutex.Unlock()
130129

131130
status := make(map[string]interface{})
132131
for client := range hub.clients {
133-
secret := client.secret
134-
currentStatus, ok := status[secret]
132+
roomID := client.roomID
133+
currentStatus, ok := status[roomID]
135134
if !ok {
136-
// Initialize status for a new secret
137-
status[secret] = map[string]interface{}{
135+
// Initialize status for a new roomID
136+
status[roomID] = map[string]interface{}{
138137
"clients": 1,
139-
"color": hub.colors[secret],
138+
"workspace": hub.workspaces[roomID],
140139
}
141140
} else {
142-
// Update the client count for an existing secret
143-
status[secret] = map[string]interface{}{
141+
// Update the client count for an existing roomID
142+
status[roomID] = map[string]interface{}{
144143
"clients": currentStatus.(map[string]interface{})["clients"].(int) + 1,
145-
"color": hub.colors[secret],
144+
"workspace": hub.workspaces[roomID],
146145
}
147146
}
148147
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { fetchQuestion } from "@/api/gateway";
2+
import { Question as QnType, StatusBody, isError } from "@/api/structs";
3+
import styles from "@/style/question.module.css";
4+
import ErrorBlock from "@/components/shared/ErrorBlock";
5+
import React from "react";
6+
import QuestionBlock from "./question";
7+
8+
type Props = {
9+
params: {
10+
question: string;
11+
roomID: string;
12+
};
13+
};
14+
15+
async function Question({ params }: Props) {
16+
const question = await fetchQuestion(params.question);
17+
18+
return (
19+
<div className={styles.wrapper}>
20+
{isError(question) ? (
21+
<ErrorBlock err={question as StatusBody} />
22+
) : (
23+
<QuestionBlock question={question as QnType} roomID={params.roomID} />
24+
)}
25+
</div>
26+
);
27+
}
28+
29+
export default Question;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
import React from "react";
3+
import { Difficulty, Question } from "@/api/structs";
4+
import Chip from "@/components/shared/Chip";
5+
import PeerprepButton from "@/components/shared/PeerprepButton";
6+
import styles from "@/style/question.module.css";
7+
import { useRouter } from "next/navigation";
8+
import { deleteQuestion } from "@/app/api/internal/questions/helper";
9+
import CollabEditor from "@/components/questionpage/CollabEditor";
10+
import DOMPurify from "dompurify";
11+
12+
interface Props {
13+
question: Question;
14+
roomID?: String;
15+
}
16+
17+
interface DifficultyChipProps {
18+
diff: Difficulty;
19+
}
20+
21+
function DifficultyChip({ diff }: DifficultyChipProps) {
22+
return diff === Difficulty.Easy ? (
23+
<Chip className={styles.easy}>Easy</Chip>
24+
) : diff === Difficulty.Medium ? (
25+
<Chip className={styles.med}>Med</Chip>
26+
) : (
27+
<Chip className={styles.hard}>Hard</Chip>
28+
);
29+
}
30+
31+
function QuestionBlock({ question, roomID }: Props) {
32+
const router = useRouter();
33+
34+
const handleDelete = async () => {
35+
if (
36+
confirm(
37+
`Are you sure you want to delete ${question.title}? (ID: ${question.id}) `
38+
)
39+
) {
40+
const status = await deleteQuestion(question.id);
41+
if (status.error) {
42+
alert(
43+
`Failed to delete question. Code ${status.status}: ${status.error}`
44+
);
45+
return;
46+
}
47+
console.log(`Successfully deleted the question.`);
48+
router.push("/questions");
49+
} else {
50+
console.log("Deletion cancelled.");
51+
}
52+
};
53+
54+
return (
55+
<>
56+
<div className={styles.qn_container}>
57+
<div className={styles.title_wrapper}>
58+
<div className={styles.label_wrapper}>
59+
<h1 className={styles.title}>
60+
Q{question.id}: {question.title}
61+
</h1>
62+
<DifficultyChip diff={question.difficulty} />
63+
</div>
64+
<PeerprepButton
65+
className={` ${styles.button}`}
66+
onClick={handleDelete}
67+
>
68+
Delete
69+
</PeerprepButton>
70+
</div>
71+
<div className={styles.label_wrapper}>
72+
<p>Topics: </p>
73+
{question.topicTags.length == 0 ? (
74+
<p>No topics listed.</p>
75+
) : (
76+
question.topicTags.map((elem, idx) => (
77+
<p key={idx} className={styles.label_shadow}>
78+
{elem}
79+
</p>
80+
))
81+
)}
82+
</div>
83+
{
84+
<div
85+
className={styles.editorHTML}
86+
dangerouslySetInnerHTML={{
87+
__html: DOMPurify.sanitize(question.content),
88+
}}
89+
/>
90+
}
91+
</div>
92+
<div className={styles.editor_container}>
93+
<CollabEditor question={question} roomID={roomID} />
94+
</div>
95+
</>
96+
);
97+
}
98+
99+
export default QuestionBlock;

peerprep/components/questionpage/CollabEditor.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useEffect } from "react";
22
import AceEditor from "react-ace";
33

44
import "ace-builds/src-noconflict/mode-python";
@@ -44,9 +44,10 @@ themes.forEach((theme) => require(`ace-builds/src-noconflict/theme-${theme}`));
4444

4545
interface Props {
4646
question: Question;
47+
roomID?: String;
4748
}
4849

49-
export default function CollabEditor({ question }: Props) {
50+
export default function CollabEditor({ question, roomID }: Props) {
5051
const [theme, setTheme] = useState("terminal");
5152
const [fontSize, setFontSize] = useState(18);
5253
const [language, setLanguage] = useState("python");
@@ -67,8 +68,10 @@ export default function CollabEditor({ question }: Props) {
6768
editor.container.style.resize = "both";
6869
};
6970

70-
const connectWebSocket = () => {
71-
const newSocket = new WebSocket("ws://localhost:4000/ws?secret=bruh");
71+
useEffect(() => {
72+
if (!roomID) return;
73+
74+
const newSocket = new WebSocket(`ws://localhost:4000/ws?roomID=${roomID}`);
7275

7376
newSocket.onopen = () => {
7477
console.log("WebSocket connection established");
@@ -79,7 +82,7 @@ export default function CollabEditor({ question }: Props) {
7982
const message = JSON.parse(event.data);
8083
console.log("Received WebSocket message:", message);
8184

82-
// Handle incoming WebSocket messages if required
85+
// Handle incoming WebSocket messages
8386
if (message.type === "content_change") {
8487
setValue(message.data); // Update the editor value
8588
}
@@ -91,10 +94,7 @@ export default function CollabEditor({ question }: Props) {
9194
};
9295

9396
setSocket(newSocket);
94-
};
95-
96-
// TODO: to be taken from question props instead
97-
// const value = question[language] ?? "// Comment"
97+
}, []);
9898

9999
return (
100100
<>
@@ -128,17 +128,12 @@ export default function CollabEditor({ question }: Props) {
128128
"border border-gray-600 bg-gray-800 text-white p-2 rounded"
129129
}
130130
/>
131-
132-
<div>
133-
<button
134-
onClick={connectWebSocket}
135-
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
136-
>
137-
{connected ? "Connected" : "Connect WebSocket"}
138-
</button>
139-
</div>
140131
</div>
141-
132+
{roomID && (
133+
<div className="w-full text-center mb-[16px]">
134+
{connected ? "Connected" : "Disconnected"}
135+
</div>
136+
)}
142137
<AceEditor
143138
mode={language}
144139
className={"editor"}

peerprep/components/questionpage/Matchmaking.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ const Matchmaking = () => {
5656
const router = useRouter();
5757
const [isMatching, setIsMatching] = useState<boolean>(false);
5858
const { difficulties, topicList } = useQuestionFilter();
59-
const [difficultyFilter, setDifficultyFilter] = useState<string>(Difficulty.Easy);
59+
const [difficultyFilter, setDifficultyFilter] = useState<string>(
60+
Difficulty.Easy
61+
);
6062
const [topicFilter, setTopicFilter] = useState<string[]>(topicList);
6163
const { userid } = useUserInfo();
6264
const timeout = useRef<NodeJS.Timeout>();
@@ -99,7 +101,7 @@ const Matchmaking = () => {
99101
console.log("Match attempted");
100102
console.debug(matchRequest);
101103

102-
// send match request
104+
// send match request
103105
const status = await findMatch(matchRequest);
104106
if (status.error) {
105107
stopTimer();
@@ -131,6 +133,8 @@ const Matchmaking = () => {
131133
User1: ${matchRes.data.user1}
132134
User2: ${matchRes.data.user2}`;
133135
window.alert(message);
136+
// redirect to question page
137+
router.push(`/questions/1/${matchRes.data.roomId}`);
134138
};
135139

136140
usePeriodicCallback(queryResource, QUERY_INTERVAL_MILLISECONDS, isMatching);
@@ -145,19 +149,27 @@ const Matchmaking = () => {
145149
<PeerprepButton onClick={handleMatch}>
146150
{isMatching ? "Cancel Match" : "Find Match"}
147151
</PeerprepButton>
148-
{!isMatching &&
149-
<PeerprepDropdown label="Difficulty"
152+
{!isMatching && (
153+
<PeerprepDropdown
154+
label="Difficulty"
150155
value={difficultyFilter}
151-
onChange={e => setDifficultyFilter(e.target.value)}
156+
onChange={(e) => setDifficultyFilter(e.target.value)}
152157
// truthfully we don't need this difficulties list, but we are temporarily including it
153-
options={difficulties} />
154-
}
155-
{!isMatching &&
156-
<PeerprepDropdown label="Topics"
158+
options={difficulties}
159+
/>
160+
)}
161+
{!isMatching && (
162+
<PeerprepDropdown
163+
label="Topics"
157164
value={topicFilter[0]}
158-
onChange={e => setTopicFilter(e.target.value === "all" ? topicList : [e.target.value])}
159-
options={topicList} />
160-
}
165+
onChange={(e) =>
166+
setTopicFilter(
167+
e.target.value === "all" ? topicList : [e.target.value]
168+
)
169+
}
170+
options={topicList}
171+
/>
172+
)}
161173
{isMatching && <ResettingStopwatch isActive={isMatching} />}
162174
</div>
163175
</div>

0 commit comments

Comments
 (0)