Skip to content

Commit d8c53ea

Browse files
committed
Set up internal API, verify local testing and fix question filter bug
1 parent 1675d46 commit d8c53ea

File tree

7 files changed

+218
-20
lines changed

7 files changed

+218
-20
lines changed

peerprep/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
NEXT_PUBLIC_QUESTION_SERVICE=
22
NEXT_PUBLIC_USER_SERVICE=
3+
NEXT_PUBLIC_MATCHING_SERVICE=
4+
NEXT_PUBLIC_STORAGE_BLOB=
35
# note NGINX will currently point to itself.
46
NEXT_PUBLIC_NGINX=
57
DEV_ENV=

peerprep/api/structs.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ export interface SigninResponse {
4848
data: UserData;
4949
}
5050

51+
export interface MatchRequest {
52+
userId: string;
53+
topicTags: string[];
54+
difficulty: string;
55+
requestTime: string;
56+
}
57+
58+
export interface MatchData {
59+
roomId: string;
60+
user1: string;
61+
user2: string;
62+
}
63+
64+
export interface MatchResponse {
65+
isMatchFound: boolean;
66+
data: MatchData;
67+
}
68+
5169
// credit - taken from Next.JS Auth tutorial
5270
export type FormState =
5371
| {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
MatchData,
3+
MatchRequest,
4+
MatchResponse,
5+
StatusBody,
6+
} from "@/api/structs";
7+
8+
// helper to be called from client to check storage blob
9+
export async function checkMatchStatus(
10+
userId: string
11+
): Promise<MatchResponse | StatusBody> {
12+
console.debug("In matching helper, checking storage blob:", userId);
13+
const res = await fetch(
14+
`${process.env.NEXT_PUBLIC_NGINX}/api/internal/matching?uid=${userId}`,
15+
{
16+
method: "GET",
17+
}
18+
);
19+
if (!res.ok) {
20+
return {
21+
error: await res.text(),
22+
status: res.status,
23+
};
24+
}
25+
const json = (await res.json()) as MatchData;
26+
const isMatchFound = true; // TODO differntiate??
27+
28+
return {
29+
isMatchFound,
30+
data: json,
31+
} as MatchResponse;
32+
}
33+
34+
export async function findMatch(
35+
matchRequest: MatchRequest
36+
): Promise<StatusBody> {
37+
console.debug(
38+
"In matching helper, posting match request",
39+
JSON.stringify(matchRequest)
40+
);
41+
const res = await fetch(
42+
`${process.env.NEXT_PUBLIC_NGINX}/api/internal/matching`,
43+
{
44+
method: "POST",
45+
body: JSON.stringify(matchRequest),
46+
}
47+
);
48+
if (!res.ok) {
49+
return {
50+
error: await res.text(),
51+
status: res.status,
52+
};
53+
}
54+
const json = await res.json();
55+
return json as StatusBody;
56+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { generateAuthHeaders, generateJSONHeaders } from "@/api/gateway";
2+
import { QuestionFullBody } from "@/api/structs";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
// all get request interpreted as getting from storage blob
6+
export async function GET(request: NextRequest) {
7+
const uid = request.nextUrl.searchParams.get("uid"); // Assuming you're passing the userId as a query parameter
8+
console.log("in route,", uid);
9+
if (!uid) {
10+
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
11+
}
12+
13+
try {
14+
const response = await fetch(
15+
`${process.env.NEXT_PUBLIC_STORAGE_BLOB}/request/${uid}`,
16+
{
17+
method: "GET",
18+
headers: generateAuthHeaders(),
19+
}
20+
);
21+
if (!response.ok) {
22+
return NextResponse.json(
23+
{
24+
error: await response.text(),
25+
status: response.status,
26+
},
27+
{ status: response.status }
28+
);
29+
}
30+
return response;
31+
} catch (err: any) {
32+
return NextResponse.json(
33+
{ error: err.message, status: 400 },
34+
{ status: 400 }
35+
);
36+
}
37+
}
38+
39+
// for matching stuff all post requests interpreted as posting matchmaking request
40+
export async function POST(request: NextRequest) {
41+
const body = await request.json();
42+
try {
43+
const response = await fetch(
44+
`${process.env.NEXT_PUBLIC_MATCHING_SERVICE}/request`,
45+
{
46+
method: "POST",
47+
body: JSON.stringify(body),
48+
headers: generateJSONHeaders(),
49+
}
50+
);
51+
if (response.ok) {
52+
return NextResponse.json(
53+
{ status: response.status },
54+
{ status: response.status }
55+
);
56+
}
57+
return NextResponse.json(
58+
{
59+
error: (await response.json())["Error adding question: "],
60+
status: response.status,
61+
},
62+
{ status: response.status }
63+
);
64+
} catch (err: any) {
65+
return NextResponse.json(
66+
{ error: err.message, status: 400 },
67+
{ status: 400 }
68+
);
69+
}
70+
}

peerprep/components/questionpage/Matchmaking.tsx

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
"use client";
2-
import React, { useEffect, useState } from "react";
2+
import React, { useEffect, useRef, useState } from "react";
33
import { useRouter } from "next/navigation";
44
import PeerprepButton from "../shared/PeerprepButton";
55
import { useQuestionFilter } from "@/contexts/QuestionFilterContext";
66
import { useUserInfo } from "@/contexts/UserInfoContext";
7+
import { isError, MatchRequest, MatchResponse } from "@/api/structs";
8+
import {
9+
checkMatchStatus,
10+
findMatch,
11+
} from "@/app/api/internal/matching/helper";
12+
import { match } from "assert";
13+
import { TIMEOUT } from "dns";
714

8-
const QUERY_INTERVAL_MILLISECONDS = 2000;
15+
const QUERY_INTERVAL_MILLISECONDS = 5000;
16+
const TIMEOUT_MILLISECONDS = 30000;
917

1018
const getMatchRequestTime = (): string => {
1119
const now = new Date();
@@ -41,29 +49,58 @@ const usePeriodicCallback = (
4149

4250
const Matchmaking = () => {
4351
const router = useRouter();
44-
const [isMatching, setIsMatching] = useState(false);
52+
const [isMatching, setIsMatching] = useState<boolean>(false);
4553
const { difficulty, topics } = useQuestionFilter();
4654
const { userid } = useUserInfo();
55+
const timeout = useRef<NodeJS.Timeout>();
4756

48-
const handleMatch = () => {
57+
const handleMatch = async () => {
4958
if (!isMatching) {
59+
// start 30s timeout
60+
timeout.current = setTimeout(() => {
61+
setIsMatching(false);
62+
console.log("Match request timed out after 30s");
63+
}, TIMEOUT_MILLISECONDS);
64+
5065
setIsMatching(true);
66+
const matchRequest: MatchRequest = {
67+
userId: userid,
68+
difficulty: difficulty,
69+
topicTags: topics,
70+
requestTime: getMatchRequestTime(),
71+
};
5172
console.log("Match attempted");
52-
console.log("Selected Difficulty:", difficulty);
53-
console.log("Selected Topics:", topics);
54-
console.debug("Request time: ", getMatchRequestTime());
55-
console.debug("User id: ", userid);
73+
console.debug(matchRequest);
74+
75+
const status = await findMatch(matchRequest);
76+
if (status.error) {
77+
console.log("Failed to find match. Cancel matching.");
78+
setIsMatching(false);
79+
return;
80+
}
81+
console.log(`Started finding match.`);
5682
} else {
83+
// if user manually stopped it clear timeout
84+
if (timeout.current) {
85+
clearTimeout(timeout.current);
86+
}
87+
5788
setIsMatching(false);
58-
console.debug("User stopped matching");
89+
console.log("User stopped matching");
5990
}
60-
61-
// username as userid?
62-
// should probably just use the questionlist selections as params
6391
};
6492

65-
const queryResource = () => {
66-
console.debug("Querying resource blob for matchmaking status");
93+
const queryResource = async () => {
94+
const res = await checkMatchStatus(userid);
95+
if (isError(res)) {
96+
// for now 404 means no match found so dont stop matching on error, let request timeout
97+
return;
98+
}
99+
setIsMatching(false);
100+
// TODO: iron out what is in a match response and sync up with collab service rooms
101+
const matchRes: MatchResponse = res as MatchResponse;
102+
console.log("Match found!");
103+
console.debug(matchRes);
67104
};
68105

69106
usePeriodicCallback(queryResource, QUERY_INTERVAL_MILLISECONDS, isMatching);
@@ -79,7 +116,7 @@ const Matchmaking = () => {
79116
{isMatching ? "Cancel Match" : "Find Match"}
80117
</PeerprepButton>
81118
{isMatching && (
82-
<div className="w-5 h-5 bg-difficulty-hard rounded-full ml-2" />
119+
<div className="w-3 h-3 bg-difficulty-hard rounded-full ml-2" />
83120
)}
84121
</div>
85122
</div>

peerprep/components/questionpage/QuestionList.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const QuestionList: React.FC = () => {
3232
// get all present topics in all qns
3333
const uniqueTopics = Array.from(
3434
new Set(data.flatMap((question) => question.topicTags))
35-
);
35+
).sort();
3636
setTopicsList(["all", ...uniqueTopics]);
3737
};
3838

@@ -54,6 +54,20 @@ const QuestionList: React.FC = () => {
5454

5555
const sortedQuestions = filteredQuestions.sort((a, b) => a.id - b.id);
5656

57+
const handleSetDifficulty = (e: React.ChangeEvent<HTMLSelectElement>) => {
58+
const diff = e.target.value;
59+
setDifficulty(diff);
60+
};
61+
62+
const handleSetTopics = (e: React.ChangeEvent<HTMLSelectElement>) => {
63+
const topic = e.target.value;
64+
if (topic === "all") {
65+
setTopics(topicsList);
66+
} else {
67+
setTopics([topic]);
68+
}
69+
};
70+
5771
return (
5872
<div className="flex-grow max-h-screen overflow-y-auto p-4">
5973
<div className="flex space-x-4 mb-4 items-end">
@@ -65,13 +79,14 @@ const QuestionList: React.FC = () => {
6579
<PeerprepDropdown
6680
label="Difficulty"
6781
value={difficulty}
68-
onChange={(e) => setDifficulty(e.target.value)}
82+
onChange={handleSetDifficulty}
6983
options={Object.keys(Difficulty).filter((key) => isNaN(Number(key)))}
7084
/>
7185
<PeerprepDropdown
7286
label="Topics"
87+
// coincidentally "all" is at the top of the list so the display works out...dumb luck!
7388
value={topics[0]}
74-
onChange={(e) => setTopics([e.target.value])}
89+
onChange={handleSetTopics}
7590
options={topicsList}
7691
/>
7792
</div>

peerprep/contexts/UserInfoContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// maybe store SAFE user info and wrap the whole damn app in it
1+
// maybe store SAFE user info and wrap the relevant client components in it (like titlebar? matchmaking?)
22
// username, userid?, userstate (matching, idle, inmenu)
33

44
"use client";
@@ -16,7 +16,7 @@ const UserInfoContext = createContext<UserInfoContextType | undefined>(
1616
export const UserInfoProvider: React.FC<{ children: ReactNode }> = ({
1717
children,
1818
}) => {
19-
const [userid, setUserid] = useState<string>("dummy-user");
19+
const [userid, setUserid] = useState<string>("test-user");
2020

2121
return (
2222
<UserInfoContext.Provider value={{ userid, setUserid }}>

0 commit comments

Comments
 (0)