Skip to content

Commit 78a7882

Browse files
authored
Merge pull request #130 from PeerPrep/kevin/table
feat: add a bunch of stuff
2 parents d1513fd + 96ebdf3 commit 78a7882

File tree

23 files changed

+2978
-2575
lines changed

23 files changed

+2978
-2575
lines changed

frontend/next.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
const nextConfig = {
33
reactStrictMode: true,
44
swcMinify: true,
5+
images: {
6+
remotePatterns: [
7+
{
8+
protocol: "https",
9+
hostname: "firebasestorage.googleapis.com",
10+
port: "",
11+
pathname: "*",
12+
},
13+
],
14+
},
515
};
616

717
module.exports = nextConfig;

frontend/src/app/admin/portal/page.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
"use client";
22

3+
import { fetchAllUsers, fetchIsAdmin, promoteToAdmin } from "@/app/api";
34
import Button from "@/app/components/button/Button";
4-
import { useState } from "react";
5+
import useAdmin from "@/app/hooks/useAdmin";
6+
import { message } from "antd";
7+
import { useRouter } from "next/navigation";
8+
import { use, useEffect, useState } from "react";
59
import Select, { MultiValue } from "react-select";
610

711
const AdminPortalPage = () => {
12+
const isAdmin = useAdmin();
13+
const router = useRouter();
14+
const [api, contextHolder] = message.useMessage();
15+
16+
type User = {
17+
uid: string;
18+
name: string;
19+
imageUrl: string | null;
20+
preferredLang: string | null;
21+
role: string;
22+
};
23+
824
interface SelectOptionType {
925
label: string;
1026
value: string;
1127
}
12-
const adminOptions: MultiValue<SelectOptionType> = [
13-
{ label: "Hello", value: "123" },
14-
{ label: "Hello2", value: "456" },
15-
];
28+
const [adminOptions, setAdminOptions] = useState<
29+
MultiValue<SelectOptionType>
30+
>([]);
31+
32+
useEffect(() => {
33+
fetchAllUsers().then((allUsers) => {
34+
setAdminOptions(
35+
allUsers.payload
36+
.filter((user: User) => user.role === "user")
37+
.map((user: User) => ({ label: user.name, value: user.uid })),
38+
);
39+
});
40+
}, [api, contextHolder]);
1641

1742
const handleSelectChange = (
1843
selectedOptions: MultiValue<SelectOptionType>,
@@ -24,8 +49,13 @@ const AdminPortalPage = () => {
2449
MultiValue<SelectOptionType>
2550
>([]);
2651

52+
if (!isAdmin) {
53+
router.push("/");
54+
}
55+
2756
return (
2857
<main className="mt-12 flex flex-col items-center justify-center">
58+
{contextHolder}
2959
<h1 className="mb-2 block text-5xl font-bold text-white underline">
3060
Admin Portal
3161
</h1>
@@ -49,7 +79,28 @@ const AdminPortalPage = () => {
4979
classNamePrefix="select"
5080
/>
5181
</section>
52-
<Button className="btn-accent" onClick={() => "todo"}>
82+
<Button
83+
className="btn-accent"
84+
onClick={() => {
85+
promoteToAdmin(selectedQnType.map((option) => option.value))
86+
.then((res) => {
87+
if (res.statusMessage.type.toLowerCase() === "success") {
88+
api.success({
89+
type: "success",
90+
content: "Successfully updated profile!",
91+
});
92+
} else {
93+
api.error({
94+
type: "error",
95+
content: "Failed to update profile :(",
96+
});
97+
}
98+
})
99+
.then(() => {
100+
setSelectedQnType([]);
101+
});
102+
}}
103+
>
53104
Grant Access
54105
</Button>
55106
</div>

frontend/src/app/admin/question/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import EditQuestionModal from "@/app/components/modal/EditQuestionModal";
2828
import topicsOptions from "../questionTypeData";
2929
import { onAuthStateChanged } from "firebase/auth";
3030
import useLogin from "@/app/hooks/useLogin";
31+
import { useRouter } from "next/navigation";
32+
import useAdmin from "@/app/hooks/useAdmin";
3133

3234
export interface QuestionType {
3335
_id?: string;
@@ -56,6 +58,13 @@ const Table = dynamic(() => import("antd/lib").then((m) => m.Table), {
5658
});
5759

5860
const QuestionPage = () => {
61+
const router = useRouter();
62+
const isAdmin = useAdmin();
63+
64+
if (!isAdmin) {
65+
router.push("/");
66+
}
67+
5968
const user = useLogin();
6069
const [api, contextHolder] = message.useMessage();
6170
const [currQn, setCurrQn] = useState<QuestionType | null>(null);
@@ -302,7 +311,7 @@ const QuestionPage = () => {
302311
</div>
303312
</dialog>
304313
<dialog id="delete_modal" className="modal">
305-
<div className="modal-box max-w-screen-xl p-6">
314+
<div className="max-w-screen modal-box p-6">
306315
<form method="dialog" className="pb">
307316
<button className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
308317

frontend/src/app/api/index.ts

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22
import { atom } from "jotai";
33
import { QuestionType } from "../admin/question/page";
4+
import { Profile } from "../hooks/useLogin";
45

56
export const firebaseTokenAtom = atom<string | null>(null);
67

@@ -10,19 +11,13 @@ export const FetchAuth = {
1011
addFirebaseToken: function (firebaseToken: string) {
1112
this.firebaseToken = firebaseToken;
1213
},
13-
getFirebaseToken: async function (timeoutInMilliseconds: number = 10 * 1000) {
14-
console.log(
15-
`Looking for firebase token... (max ${timeoutInMilliseconds} ms left)`,
16-
);
17-
const intervalInMs = 100;
18-
while (!this.firebaseToken && timeoutInMilliseconds > 0) {
19-
timeoutInMilliseconds -= intervalInMs;
20-
console.log(
21-
`Waiting for firebase token... (max ${timeoutInMilliseconds} ms left)`,
14+
getFirebaseToken: async function (timeoutInMilliseconds: number = 100) {
15+
while (!this.firebaseToken) {
16+
await new Promise((resolve) =>
17+
setTimeout(resolve, timeoutInMilliseconds),
2218
);
23-
await new Promise((resolve) => setTimeout(resolve, intervalInMs));
2419
}
25-
console.log(`Found firebase token. ${this.firebaseToken}`);
20+
2621
return this.firebaseToken;
2722
},
2823
fetch: async function (
@@ -40,7 +35,12 @@ export const FetchAuth = {
4035
options.headers = headers;
4136

4237
// Perform the fetch request with the modified options
43-
return fetch(url, options);
38+
const res = await fetch(url, options);
39+
if (!res.ok) {
40+
// console.log(res);
41+
// throw Error();
42+
}
43+
return res;
4444
},
4545
};
4646

@@ -53,12 +53,66 @@ export const fetchQuestionDescriptionUrl = async (qnId: string) => {
5353
);
5454
};
5555

56+
export const fetchAllQuestionsDoneByUser = async () => {
57+
const { payload } = (await FetchAuth.fetch(`${API_URL}/users/activity/`).then(
58+
(res) => {
59+
res.json();
60+
},
61+
)) as any;
62+
const questionIds = payload.join("-");
63+
// console.log({ res });
64+
return await FetchAuth.fetch(
65+
`${API_URL}/questions/group/${questionIds}`,
66+
).then((res) => {
67+
res.json();
68+
});
69+
};
70+
5671
export const fetchAllQuestionsUrl = async () => {
5772
return await FetchAuth.fetch(`${API_URL}/questions/`).then((res) =>
5873
res.json(),
5974
);
6075
};
6176

77+
export const completeQuestion = async (questionId: string) => {
78+
return await FetchAuth.fetch(`${API_URL}/users/activity/`, {
79+
method: "POST",
80+
headers: {
81+
"Content-Type": "application/json",
82+
},
83+
body: JSON.stringify({
84+
questionId,
85+
}),
86+
}).then((res) => res.json());
87+
};
88+
89+
export const promoteToAdmin = async (userId: string[]) => {
90+
return await FetchAuth.fetch(`${API_URL}/users/admin/update`, {
91+
method: "POST",
92+
headers: {
93+
"Content-Type": "application/json",
94+
},
95+
body: JSON.stringify({
96+
role: "admin",
97+
uids: userId,
98+
}),
99+
}).then((res) => res.json());
100+
};
101+
102+
export const fetchAllUsers = async () => {
103+
return await FetchAuth.fetch(`${API_URL}/users/admin/profiles`).then((res) =>
104+
res.json(),
105+
);
106+
};
107+
108+
export const fetchIsAdmin = async () => {
109+
return await FetchAuth.fetch(`${API_URL}/users/admin/profiles`)
110+
.then((res) => res.json())
111+
.then((res) => {
112+
return res.statusMessage.type.toLowerCase() === "success";
113+
});
114+
};
115+
62116
export const createQuestionUrl = async (newQuestion: QuestionType) => {
63117
return FetchAuth.fetch(`${API_URL}/questions/`, {
64118
method: "POST",
@@ -85,13 +139,46 @@ export const deleteQuestionUrl = async (questionId: string) => {
85139
}).then((res) => res.json());
86140
};
87141

142+
export const deleteProfileUrl = async () => {
143+
return FetchAuth.fetch(`${API_URL}/users/profile`, {
144+
method: "DELETE",
145+
}).then((res) => res.json());
146+
};
147+
148+
interface ProfileResponse {
149+
payload: Profile;
150+
statusMessage: {
151+
type: "success" | "error";
152+
message: string;
153+
};
154+
}
155+
156+
export async function fetchProfileUrl(): Promise<ProfileResponse> {
157+
return FetchAuth.fetch(`${API_URL}/users/profile`, { method: "GET" }).then(
158+
(res) => res.json(),
159+
);
160+
}
161+
162+
export async function updateProfileUrl(
163+
name: string | null,
164+
preferredLang: string | null,
165+
profileImage: File | null,
166+
): Promise<ProfileResponse> {
167+
const body = new FormData();
168+
if (name) body.set("name", name);
169+
if (preferredLang) body.set("preferredLang", preferredLang);
170+
if (profileImage) body.set("image", profileImage);
171+
return FetchAuth.fetch(`${API_URL}/users/profile`, {
172+
method: "POST",
173+
body,
174+
}).then((res) => res.json());
175+
}
88176
const executorURL = "https://peerprep.sivarn.com/api/v1/execute";
89177
export const executeCode = async (code: string, lang: string) => {
90178
const res = await fetch(`${executorURL}/${lang}`, {
91179
method: "POST",
92180
body: code,
93181
});
94182
const data = res.text();
95-
console.log(data);
96183
return data;
97184
};

frontend/src/app/components/code-editor/CodeEditor.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { yCollab } from "y-codemirror.next";
1515
import { SocketIOProvider } from "y-socket.io";
1616
import * as Y from "yjs";
1717
import Tabs from "../tab/Tabs";
18+
import { fetchProfileUrl } from "@/app/api";
1819

1920
const CodeMirror = dynamic(() => import("@uiw/react-codemirror"), {
2021
ssr: false,
@@ -33,19 +34,6 @@ if (typeof window !== "undefined") {
3334
desiredWidth = window.innerWidth >= 1024 ? "50vw" : "90vw";
3435
}
3536

36-
// TODO: idk what FE plan is for this, so i just slapped a random text box thing here.
37-
const UiElementOnClose = () => {
38-
return (
39-
<div className="flex items-center gap-4 bg-slate-800 p-2">
40-
<ExclamationCircleFilled />
41-
Your room has been closed.
42-
<Button className="btn-accent" onClick={() => window.location.reload()}>
43-
Restart
44-
</Button>
45-
</div>
46-
);
47-
};
48-
4937
const codeLangAtomWrapper = atom(
5038
(get) => get(codeLangAtom),
5139
(_get, set, lang: string) => {
@@ -79,6 +67,9 @@ const CodeMirrorEditor = ({
7967
const [extensions, setExtensions] = useState<any>([]);
8068

8169
useEffect(() => {
70+
fetchProfileUrl().then((res) => {
71+
setSelectedLanguage(res.payload.preferredLang || "python");
72+
});
8273
if (!innkeeperUrl) {
8374
console.error(
8475
"NEXT_PUBLIC_PEERPREP_INNKEEPER_SOCKET_URL not set in .env",
@@ -204,7 +195,6 @@ const CodeMirrorEditor = ({
204195
</select>
205196
</div>
206197
</div>
207-
{isMatched !== "MATCHED" && <UiElementOnClose />}
208198
<CodeMirror
209199
className="max-h-[70svw] w-[90svw] lg:w-[50svw]"
210200
height={`${editorHeight}px`}

frontend/src/app/components/matching/MatchingPage.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import QueueButton from "../button/QueueButton";
44
import { QuestionType } from "../../admin/question/page";
55
import { atom, useAtom } from "jotai";
66
import { innkeeperWriteAtom } from "@/libs/room-jotai";
7+
import { fetchAllQuestionsDoneByUser } from "@/app/api";
8+
import { useQuery } from "@tanstack/react-query";
9+
import { Skeleton, Table } from "antd";
710

811
const sendMatchRequestAtom = atom(
912
null,
@@ -91,8 +94,18 @@ const MatchingPage = () => {
9194
},
9295
];
9396

97+
const {
98+
data: allQuestions,
99+
isLoading: allQuestionsLoading,
100+
refetch: refetchAllQuestions,
101+
} = useQuery(["activityQuestions"], () => {
102+
return fetchAllQuestionsDoneByUser();
103+
});
104+
105+
console.log({ allQuestions });
106+
94107
return (
95-
<main className="flex h-full items-center justify-center">
108+
<main className="flex h-full flex-col items-center justify-center">
96109
<section className="flex items-center gap-4">
97110
<label>
98111
<span>Difficulty Setting:</span>
@@ -128,6 +141,18 @@ const MatchingPage = () => {
128141
</div>
129142
<QueueButton enterQueue={() => sendMatchRequest(difficulty)} />
130143
</section>
144+
<div className="m-7">
145+
<h1 className="mb-2 block text-5xl font-bold text-white underline">
146+
Completed Questions
147+
</h1>
148+
<Table
149+
className="mt-4"
150+
bordered
151+
columns={activityTableColumns}
152+
dataSource={allQuestions as any}
153+
pagination={{ position: ["bottomCenter"] }}
154+
/>
155+
</div>
131156
</main>
132157
);
133158
};

0 commit comments

Comments
 (0)