Skip to content

Commit d8f75ac

Browse files
authored
Merge pull request #34 from CS3219-AY2425S1/add-match-timer
Add matching button functionality
2 parents 8454cf8 + 6770d98 commit d8f75ac

File tree

13 files changed

+170
-56
lines changed

13 files changed

+170
-56
lines changed

peerprep/api/gateway.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { cookies } from "next/headers";
2-
import { LoginResponse, Question, SigninResponse, StatusBody } from "./structs";
2+
import { LoginResponse, Question, UserServiceResponse, StatusBody } from "./structs";
33
import DOMPurify from "isomorphic-dompurify";
44

55
export function generateAuthHeaders() {
66
return {
7-
Authorization: `Bearer ${cookies().get("session")}`,
8-
};
7+
"Authorization": `Bearer ${cookies().get("session")?.value}`,
8+
};;
99
}
1010

1111
export function generateJSONHeaders() {
@@ -75,7 +75,7 @@ export async function postSignupUser(validatedFields: {
7575
username: string;
7676
email: string;
7777
password: string;
78-
}): Promise<SigninResponse | StatusBody> {
78+
}): Promise<UserServiceResponse | StatusBody> {
7979
try {
8080
console.log(JSON.stringify(validatedFields));
8181
const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/users`, {
@@ -97,3 +97,23 @@ export async function postSignupUser(validatedFields: {
9797
return { error: err.message, status: 400 };
9898
}
9999
}
100+
101+
export async function verifyUser(): Promise<UserServiceResponse | StatusBody> {
102+
try {
103+
const res = await fetch(
104+
`${process.env.NEXT_PUBLIC_USER_SERVICE}/auth/verify-token`,
105+
{
106+
method: "GET",
107+
headers: generateAuthHeaders(),
108+
}
109+
);
110+
const json = await res.json();
111+
112+
if (!res.ok) {
113+
return { error: json.message, status: res.status };
114+
}
115+
return json;
116+
} catch (err: any) {
117+
return { error: err.message, status: 400 };
118+
}
119+
}

peerprep/api/structs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface LoginResponse {
4343
data: UserDataAccessToken;
4444
}
4545

46-
export interface SigninResponse {
46+
export interface UserServiceResponse {
4747
message: string;
4848
data: UserData;
4949
}

peerprep/app/actions/server_actions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"use server";
2-
import { getSessionLogin, postSignupUser } from "@/api/gateway";
2+
import { getSessionLogin, postSignupUser, verifyUser } from "@/api/gateway";
33
// defines the server-sided login action.
44
import {
55
SignupFormSchema,
66
LoginFormSchema,
77
FormState,
88
isError,
9+
UserServiceResponse,
910
} from "@/api/structs";
10-
import { createSession } from "@/app/actions/session";
11+
import { createSession, expireSession } from "@/app/actions/session";
1112
import { redirect } from "next/navigation";
13+
import { cookies } from "next/headers";
1214

1315
// credit - taken from Next.JS Auth tutorial
1416
export async function signup(state: FormState, formData: FormData) {
@@ -59,3 +61,18 @@ export async function login(state: FormState, formData: FormData) {
5961
console.log(json.error);
6062
}
6163
}
64+
65+
export async function hydrateUid(): Promise<undefined | string> {
66+
if (!cookies().has("session")) {
67+
console.log("No session found - triggering switch back to login page.");
68+
redirect("/auth/login");
69+
}
70+
const json = await verifyUser();
71+
if (isError(json)) {
72+
console.log("Failed to fetch user ID.");
73+
console.log(`Error ${json.status}: ${json.error}`);
74+
redirect("/api/internal/auth/expire");
75+
}
76+
77+
return json.data.id;
78+
}

peerprep/app/actions/session.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export async function createSession(accessToken: string) {
1111
path: "/",
1212
});
1313
}
14+
15+
export async function expireSession() {
16+
cookies().delete("session");
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expireSession } from "@/app/actions/session";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export async function GET(request: NextRequest) {
5+
await expireSession();
6+
const url = request.nextUrl.clone();
7+
url.pathname = "/auth/login";
8+
return NextResponse.redirect(url);
9+
}

peerprep/app/api/internal/questions/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { generateAuthHeaders, generateJSONHeaders } from "@/api/gateway";
2-
import { QuestionFullBody } from "@/api/structs";
32
import { NextRequest, NextResponse } from "next/server";
43

54
export async function GET() {

peerprep/app/questions/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import QuestionList from "@/components/questionpage/QuestionList";
33
import Matchmaking from "@/components/questionpage/Matchmaking";
44
import { QuestionFilterProvider } from "@/contexts/QuestionFilterContext";
55
import { UserInfoProvider } from "@/contexts/UserInfoContext";
6+
import { hydrateUid } from "../actions/server_actions";
67

7-
const QuestionsPage = () => {
8+
async function QuestionsPage() {
9+
const userId = await hydrateUid();
810
return (
9-
<UserInfoProvider>
11+
<UserInfoProvider userid={userId}>
1012
<QuestionFilterProvider>
1113
<Matchmaking></Matchmaking>
1214
<QuestionList></QuestionList>

peerprep/components/questionpage/Matchmaking.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ 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";
7+
import {
8+
Difficulty,
9+
isError,
10+
MatchRequest,
11+
MatchResponse,
12+
} from "@/api/structs";
813
import {
914
checkMatchStatus,
1015
findMatch,
1116
} from "@/app/api/internal/matching/helper";
12-
import { match } from "assert";
13-
import { TIMEOUT } from "dns";
17+
import ResettingStopwatch from "../shared/ResettingStopwatch";
18+
import PeerprepDropdown from "../shared/PeerprepDropdown";
1419

1520
const QUERY_INTERVAL_MILLISECONDS = 5000;
1621
const TIMEOUT_MILLISECONDS = 30000;
@@ -50,7 +55,9 @@ const usePeriodicCallback = (
5055
const Matchmaking = () => {
5156
const router = useRouter();
5257
const [isMatching, setIsMatching] = useState<boolean>(false);
53-
const { difficulty, topics } = useQuestionFilter();
58+
const [difficultyFilter, setDifficultyFilter] = useState<string>(Difficulty.Easy);
59+
const [topicFilter, setTopicFilter] = useState<string[]>(["all"]);
60+
const { difficulties, topicList } = useQuestionFilter();
5461
const { userid } = useUserInfo();
5562
const timeout = useRef<NodeJS.Timeout>();
5663

@@ -62,6 +69,17 @@ const Matchmaking = () => {
6269
}
6370
};
6471

72+
const getMatchMakingRequest = (): MatchRequest => {
73+
const matchRequest: MatchRequest = {
74+
userId: userid,
75+
difficulty: difficultyFilter,
76+
topicTags: topicFilter,
77+
requestTime: getMatchRequestTime(),
78+
};
79+
80+
return matchRequest;
81+
};
82+
6583
const handleMatch = async () => {
6684
if (!isMatching) {
6785
setIsMatching(true);
@@ -73,12 +91,7 @@ const Matchmaking = () => {
7391
}, TIMEOUT_MILLISECONDS);
7492

7593
// assemble the match request
76-
const matchRequest: MatchRequest = {
77-
userId: userid,
78-
difficulty: difficulty,
79-
topicTags: topics,
80-
requestTime: getMatchRequestTime(),
81-
};
94+
const matchRequest = getMatchMakingRequest();
8295
console.log("Match attempted");
8396
console.debug(matchRequest);
8497

@@ -128,9 +141,20 @@ const Matchmaking = () => {
128141
<PeerprepButton onClick={handleMatch}>
129142
{isMatching ? "Cancel Match" : "Find Match"}
130143
</PeerprepButton>
131-
{isMatching && (
132-
<div className="w-3 h-3 bg-difficulty-hard rounded-full ml-2" />
133-
)}
144+
{!isMatching &&
145+
<PeerprepDropdown label="Difficulty"
146+
value={difficultyFilter}
147+
onChange={e => setDifficultyFilter(e.target.value)}
148+
// truthfully we don't need this difficulties list, but we are temporarily including it
149+
options={difficulties} />
150+
}
151+
{!isMatching &&
152+
<PeerprepDropdown label="Topics"
153+
value={topicFilter[0]}
154+
onChange={e => setTopicFilter(e.target.value === "all" ? topicList : [e.target.value])}
155+
options={topicList} />
156+
}
157+
{isMatching && <ResettingStopwatch isActive={isMatching} />}
134158
</div>
135159
</div>
136160
);

peerprep/components/questionpage/QuestionList.tsx

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import React, { useEffect, useState } from "react";
33
import QuestionCard from "./QuestionCard";
4-
import { Question, StatusBody, Difficulty, isError } from "@/api/structs";
4+
import { Question, Difficulty, isError } from "@/api/structs";
55
import PeerprepDropdown from "../shared/PeerprepDropdown";
66
import PeerprepSearchBar from "../shared/PeerprepSearchBar";
77
import { useQuestionFilter } from "@/contexts/QuestionFilterContext";
@@ -10,9 +10,12 @@ const QuestionList: React.FC = () => {
1010
const [questions, setQuestions] = useState<Question[]>([]);
1111
const [loading, setLoading] = useState(true);
1212
const [searchFilter, setSearchFilter] = useState<string>("");
13-
const [topicsList, setTopicsList] = useState<string[]>(["all"]);
13+
const [difficultyFilter, setDifficultyFilter] = useState<string>(
14+
Difficulty.All
15+
);
16+
const [topicFilter, setTopicFilter] = useState<string>("all");
1417

15-
const { difficulty, setDifficulty, topics, setTopics } = useQuestionFilter();
18+
const { topicList, setTopicList } = useQuestionFilter();
1619

1720
useEffect(() => {
1821
const fetchQuestions = async () => {
@@ -33,19 +36,18 @@ const QuestionList: React.FC = () => {
3336
const uniqueTopics = Array.from(
3437
new Set(data.flatMap((question) => question.topicTags))
3538
).sort();
36-
setTopicsList(["all", ...uniqueTopics]);
37-
setTopics(["all", ...uniqueTopics]);
39+
setTopicList(["all", ...uniqueTopics]);
3840
};
3941

4042
fetchQuestions();
4143
}, []);
4244

4345
const filteredQuestions = questions.filter((question) => {
4446
const matchesDifficulty =
45-
difficulty === Difficulty.All ||
46-
Difficulty[question.difficulty] === difficulty;
47+
difficultyFilter === Difficulty.All ||
48+
Difficulty[question.difficulty] === difficultyFilter;
4749
const matchesTopic =
48-
topics[0] === "all" || (question.topicTags ?? []).includes(topics[0]);
50+
topicFilter === "all" || (question.topicTags ?? []).includes(topicFilter);
4951
const matchesSearch =
5052
searchFilter === "" ||
5153
(question.title ?? "").toLowerCase().includes(searchFilter.toLowerCase());
@@ -57,16 +59,12 @@ const QuestionList: React.FC = () => {
5759

5860
const handleSetDifficulty = (e: React.ChangeEvent<HTMLSelectElement>) => {
5961
const diff = e.target.value;
60-
setDifficulty(diff);
62+
setDifficultyFilter(diff);
6163
};
6264

6365
const handleSetTopics = (e: React.ChangeEvent<HTMLSelectElement>) => {
6466
const topic = e.target.value;
65-
if (topic === "all") {
66-
setTopics(topicsList);
67-
} else {
68-
setTopics([topic]);
69-
}
67+
setTopicFilter(topic);
7068
};
7169

7270
return (
@@ -79,16 +77,15 @@ const QuestionList: React.FC = () => {
7977
/>
8078
<PeerprepDropdown
8179
label="Difficulty"
82-
value={difficulty}
80+
value={difficultyFilter}
8381
onChange={handleSetDifficulty}
8482
options={Object.keys(Difficulty).filter((key) => isNaN(Number(key)))}
8583
/>
8684
<PeerprepDropdown
8785
label="Topics"
88-
// coincidentally "all" is at the top of the list so the display works out...dumb luck!
89-
value={topics[0]}
86+
value={topicFilter}
9087
onChange={handleSetTopics}
91-
options={topicsList}
88+
options={topicList}
9289
/>
9390
</div>
9491

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useState, useEffect } from "react";
2+
3+
interface ResettingStopwatchProps {
4+
isActive: boolean;
5+
}
6+
7+
// pass isActive from parent component
8+
//
9+
const ResettingStopwatch: React.FC<ResettingStopwatchProps> = ({
10+
isActive,
11+
}) => {
12+
const [elapsedTime, setElapsedTime] = useState<number>(0);
13+
14+
useEffect(() => {
15+
let interval: NodeJS.Timeout | null = null;
16+
17+
if (isActive) {
18+
interval = setInterval(() => {
19+
setElapsedTime((prevTime) => prevTime + 1);
20+
}, 1000);
21+
}
22+
23+
return () => {
24+
if (interval) clearInterval(interval);
25+
setElapsedTime(0);
26+
};
27+
}, [isActive]);
28+
29+
const formatTime = (time: number) => {
30+
const minutes = Math.floor(time / 60);
31+
const seconds = time % 60;
32+
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
33+
};
34+
35+
return <div>{formatTime(elapsedTime)}</div>;
36+
};
37+
38+
export default ResettingStopwatch;

0 commit comments

Comments
 (0)