diff --git a/api_gateway/src/proxy/service_addresses.ts b/api_gateway/src/proxy/service_addresses.ts index adc30235..054b40ba 100644 --- a/api_gateway/src/proxy/service_addresses.ts +++ b/api_gateway/src/proxy/service_addresses.ts @@ -5,4 +5,4 @@ export const questionServiceUrl = isLocal ? "http://localhost:8080" : "http://qu export const userServiceHostUrl = isLocal ? "http://localhost:8081" : "http://user-service:8081/"; export const matchServiceHostUrl = isLocal ? "http://localhost:8082" : "http://matching-service:8082" export const collabServiceHostUrl = isLocal ? "http://localhost:8083" : "http://collab-service:8083" -export const executionServiceHostUrl = isLocal ? "http://localhost:8090" : "http://code-execution:8090"; \ No newline at end of file +export const executionServiceHostUrl = isLocal ? "http://localhost:8090" : "http://code-execution:8090/"; \ No newline at end of file diff --git a/code_execution/src/executor_client.ts b/code_execution/src/executor_client.ts index 5eb87374..610c50a1 100644 --- a/code_execution/src/executor_client.ts +++ b/code_execution/src/executor_client.ts @@ -2,8 +2,9 @@ import axios from "axios"; import { createBatchSubmission, getQnStdInOut } from "./testcases"; import { callbacks, langToId } from "./shared"; import { judge0Result, judge0submission, submissionResult } from "./types"; +import { submitSubmission } from "./submission_client"; -const JUDGE_API_URL = "https://judge0-ce.p.rapidapi.com"; //"http://judge0-server:2358/submissions"; +const JUDGE_API_URL = "http://peerprep-g10.com:2358"; //"https://judge0-ce.p.rapidapi.com"; const API_KEY = process.env.JUDGE0_API_KEY; async function submitIndividual(submission: judge0submission) { @@ -13,7 +14,6 @@ async function submitIndividual(submission: judge0submission) { { headers: { "Content-Type": "application/json", - "X-Rapidapi-Key": API_KEY, }, params: { base64_encoded: "true" }, } @@ -29,7 +29,6 @@ async function submitCode(submission: judge0submission[]) { { headers: { "Content-Type": "application/json", - "X-Rapidapi-Key": API_KEY, }, params: { base64_encoded: "true" }, } @@ -52,9 +51,6 @@ function pollIndividualTillComplete( ) { setTimeout(() => { const options = { - headers: { - "X-Rapidapi-Key": API_KEY, - }, params: { fields: "status", }, @@ -114,7 +110,6 @@ async function delSubmissions(tokens: string[]) { params: { fields: "status_id" }, headers: { "X-Judge0-User": "Auth-Judge0-User", - "X-Rapidapi-Key": API_KEY, }, }) ) @@ -155,8 +150,9 @@ export async function runSubmission( lang: string, qn__id: string, source_code: string, - userid: string + userid: number ) { + const submissionTime = new Date(); const resDat: submissionResult = { completed: false, evaluated: 0, @@ -190,4 +186,6 @@ export async function runSubmission( resDat.verdict = resDat.verdict === "Unknown" ? "Accepted" : resDat.verdict; resDat.completed = true; -} \ No newline at end of file + + submitSubmission(resDat.verdict, lang, qn__id, userid, source_code, submissionTime); // runs in the bg +} diff --git a/code_execution/src/index.ts b/code_execution/src/index.ts index e2ba0f4c..cbfb2055 100644 --- a/code_execution/src/index.ts +++ b/code_execution/src/index.ts @@ -38,7 +38,7 @@ app.get("/api/code/result/:token", async (req, res) => { return res.status(400).json({ error: "id not found" }); } - res.json(response); + res.status(200).json(response); } catch (error) { console.error(error); res.status(500).json({ error: "An error occurred" }); diff --git a/code_execution/src/shared.ts b/code_execution/src/shared.ts index e2a92df1..01b2320f 100644 --- a/code_execution/src/shared.ts +++ b/code_execution/src/shared.ts @@ -4,6 +4,6 @@ export const callbacks: callbackMapping = {}; export const langToId: { [key: string]: number } = { "c++17": 76, - "python3": 92, - "java": 91, -} + python3: 71, + java: 62, +}; diff --git a/code_execution/src/submission_client.ts b/code_execution/src/submission_client.ts new file mode 100644 index 00000000..9dbef459 --- /dev/null +++ b/code_execution/src/submission_client.ts @@ -0,0 +1,45 @@ +import axios from "axios"; +import { judge0Result, submissionResult } from "./types"; + +type Question = { + id: number; + _id: string; + title: string; + description: string; + topics: string[]; + difficulty: number; +}; + +const fetchQn = async (id: string) => { + const response = await axios.get( + `http://question-service:8080/api/questions/${id}` + ); + return response.data; +}; + +export const submitSubmission = async ( + verdict: string, + language: string, + qid: string, + userId: number, + sourceCode: string, + time: Date +) => { + const question = await fetchQn(qid); + const submission = { + question__id: question._id, + userId: userId, + questionTitle: question.title, + questionId: question.id, + difficulty: question.difficulty, + topics: question.topics, + verdict: verdict, + sourceCode: sourceCode, + language: language, + answeredAt: time.toISOString(), + }; + await axios.post( + `http://user-service:8081/api/users/${userId}/addquestions`, + submission + ); +}; diff --git a/frontend/src/api/code.ts b/frontend/src/api/code.ts index 5727524e..2fd5c866 100644 --- a/frontend/src/api/code.ts +++ b/frontend/src/api/code.ts @@ -7,7 +7,7 @@ export async function submitCodeForExecution(data_payload: any) { } export async function getExecutionResult(token : string) { - const res = await apiGatewayClient.post(`/api/code/result/${token}`); + const res = await apiGatewayClient.get(`/api/code/result/${token}`); return res; } \ No newline at end of file diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 71e07651..9f00ffe1 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -8,8 +8,8 @@ import { apiGatewayClient } from "./gateway"; * @returns An array of completed questions. */ export async function fetchUserCompletedQuestions( - userId: String -): Promise { + userId: number +): Promise { try { const response: AxiosResponse = await apiGatewayClient.get( `/api/users/${userId}/questions` @@ -19,14 +19,16 @@ export async function fetchUserCompletedQuestions( const completedQuestions: SolvedQuestion[] = resData.map( (q: any) => new SolvedQuestion( - q._id, - q.id, - q.title, - q.description, + q.question__id, + q.questionId, + q.questionTitle, + "", // not impt q.topics, q.difficulty, - false, // Default value for solved - undefined // Default value for solvedDate + q.verdict, + q.sourceCode, + q.language, + q.answeredAt // Default value for solvedDate ) ); @@ -38,50 +40,52 @@ export async function fetchUserCompletedQuestions( } } -/** - * Add a new question to the user's completed questions. - * @param userId - The ID of the user to whom you want to add the question. - * @param questionId - The ID of the question you want to add. - * @param complexity - The complexity of the question. - * @param category - The category of the question. - * @returns The added question. - */ -export async function addUserQuestion( - userId: number, - questionId: string, - complexity: number, - category: string[] -): Promise { - try { - const response: AxiosResponse = await apiGatewayClient.post( - `/api/users/${userId}/addquestions`, - { - userId, - questionId, - complexity, - category, - } - ); +// /** +// * Add a new question to the user's completed questions. +// * @param userId - The ID of the user to whom you want to add the question. +// * @param questionId - The ID of the question you want to add. +// * @param complexity - The complexity of the question. +// * @param category - The category of the question. +// * @returns The added question. +// */ +// export async function addUserQuestion( +// userId: number, +// questionId: string, +// complexity: number, +// category: string[] +// ): Promise { +// try { +// const response: AxiosResponse = await apiGatewayClient.post( +// `/api/users/${userId}/addquestions`, +// { +// userId, +// questionId, +// complexity, +// category, +// } +// ); - const resData = response.data; - const addedQuestion: SolvedQuestion = new SolvedQuestion( - resData._id, - resData.id, - resData.title, - resData.description, - resData.topics, - resData.difficulty, - false, // Default value for solved - undefined // Default value for solvedDate - ); +// const resData = response.data; +// const addedQuestion: SolvedQuestion = new SolvedQuestion( +// resData._id, +// resData.id, +// resData.title, +// resData.description, +// resData.topics, +// resData.difficulty, +// resData.verdict, +// resData.sourceCode, +// resData.language, +// undefined // Default value for solvedDate +// ); - return addedQuestion; - } catch (error) { - // Handle errors appropriately, e.g., log the error or throw it to be handled by the caller. - console.error(error); - throw error; - } -} +// return addedQuestion; +// } catch (error) { +// // Handle errors appropriately, e.g., log the error or throw it to be handled by the caller. +// console.error(error); +// throw error; +// } +// } /** diff --git a/frontend/src/components/MatchMakeBtn/MatchMakeBtn.component.tsx b/frontend/src/components/MatchMakeBtn/MatchMakeBtn.component.tsx index 7b9def55..5ebd7874 100644 --- a/frontend/src/components/MatchMakeBtn/MatchMakeBtn.component.tsx +++ b/frontend/src/components/MatchMakeBtn/MatchMakeBtn.component.tsx @@ -16,14 +16,7 @@ import { useMatchmake } from "../../contexts/matchmake.context"; import { AiOutlineDisconnect as DisconnectIcon } from "react-icons/ai"; import { useSelector } from "react-redux"; import { selectUser } from "../../reducers/authSlice"; - -const diffRange = [ - [0, 2.9], - [3, 5.9], - [6, 7.9], - [8, 9.9], - [0, 9.9], -]; +import { diffRanges } from "../../helper/DifficultyFilterHelper"; const MatchMakeBtn = () => { const user = useSelector(selectUser); @@ -79,21 +72,14 @@ const MatchMakeBtn = () => { )} - findMatch(diffRange[0][0], diffRange[0][1])}> - Basic - - findMatch(diffRange[1][0], diffRange[1][1])}> - Simple - - findMatch(diffRange[2][0], diffRange[2][1])}> - Medium - - findMatch(diffRange[3][0], diffRange[3][1])}> - Hard - - findMatch(diffRange[4][0], diffRange[4][1])}> - All - + {diffRanges.map((dr) => ( + findMatch(dr.range[0], dr.range[1])} + id={dr.difficulty} + > + {dr.difficulty} + + ))} ); diff --git a/frontend/src/components/ProgressBar/ProgressBar.component.tsx b/frontend/src/components/ProgressBar/ProgressBar.component.tsx index c78d7c68..e3574e4f 100644 --- a/frontend/src/components/ProgressBar/ProgressBar.component.tsx +++ b/frontend/src/components/ProgressBar/ProgressBar.component.tsx @@ -1,135 +1,141 @@ import React, { useEffect, useState } from "react"; -import { Flex, Text, CircularProgress, CircularProgressLabel, Progress, Card, Th, TableContainer, Table } from "@chakra-ui/react"; +import { + Flex, + Text, + CircularProgress, + CircularProgressLabel, + Progress, + Card, + Th, + TableContainer, + Table, + ProgressProps, + Box, + CardHeader, + Heading, + HStack, + Stat, + StatLabel, + StatNumber, + StatHelpText, + Center, + VStack, +} from "@chakra-ui/react"; import { fetchAllQuestions } from "../../api/questions"; import { Question } from "../../models/Question.model"; import { useProfile } from "../../contexts/profileContext"; +import { + diffRanges, + mapRangeToDifficulty, +} from "../../helper/DifficultyFilterHelper"; +type info = { + name: string; + percentage: number; + solved: number; + total: number; + scheme?: string; +}; const ProgressBar = () => { - const { solvedQuestions } = useProfile(); - const [easyCount, setEasyCount] = useState(0); - const [mediumCount, setMediumCount] = useState(0); - const [hardCount, setHardCount] = useState(0); - const [totalSolved, setTotalSolved] = useState(0); - const [totalEasy, setTotalEasy] = useState(0); - const [totalMedium, setTotalMedium] = useState(0); - const [totalHard, setTotalHard] = useState(0); - const [totalQuestions, setTotalQuestions] = useState(0); - const [easyPercentage, setEasyPercentage] = useState(0); - const [mediumPercentage, setMediumPercentage] = useState(0); - const [hardPercentage, setHardPercentage] = useState(0); - const [totalPercentage, setTotalPercentage] = useState(0); + const [datas, setDatas] = useState([]); useEffect(() => { - async function fetchAndCategorizeQuestions() { + (async () => { try { const allQuestions: Question[] = await fetchAllQuestions(); - // Categorize questions into easy, medium, and hard - const easyQuestions = allQuestions.filter((question) => question.difficulty >= 0 && question.difficulty < 3.5).length; - const mediumQuestions = allQuestions.filter((question) => question.difficulty >= 3.5 && question.difficulty < 7).length; - const hardQuestions = allQuestions.filter((question) => question.difficulty >= 7 && question.difficulty <= 10).length; - - const userEasyQuestions = solvedQuestions.filter((question) => question.difficulty >= 0 && question.difficulty < 3.5).length; - const userMediumQuestions = solvedQuestions.filter((question) => question.difficulty >= 3.5 && question.difficulty < 7).length; - const userHardQuestions = solvedQuestions.filter((question) => question.difficulty >= 7 && question.difficulty <= 10).length; - - setTotalEasy(easyQuestions); - setTotalMedium(mediumQuestions); - setTotalHard(hardQuestions); - setTotalQuestions(easyQuestions + mediumQuestions + hardQuestions); - setEasyCount(userEasyQuestions); - setMediumCount(userMediumQuestions); - setHardCount(userHardQuestions); - setTotalSolved(userEasyQuestions + userMediumQuestions + userHardQuestions); - - // Calculate percentages here - const easyPercentage = (userEasyQuestions / easyQuestions) * 100; - const mediumPercentage = (userMediumQuestions / mediumQuestions) * 100; - const hardPercentage = (userHardQuestions / hardQuestions) * 100; - const totalPercentage = (totalSolved / totalQuestions) * 100; - - // Set state for percentages - setEasyPercentage(easyPercentage); - setMediumPercentage(mediumPercentage); - setHardPercentage(hardPercentage); - setTotalPercentage(totalPercentage); - - // Hard coded values for testing - // setTotalEasy(10); - // setTotalMedium(10); - // setTotalHard(10); - // setTotalQuestions(30); - // setEasyCount(8); - // setMediumCount(7); - // setHardCount(3); - // setTotalSolved(18); - // setEasyPercentage(80); - // setMediumPercentage(70); - // setHardPercentage(30); - // setTotalPercentage(60); + const res: info[] = diffRanges.map((dr) => { + const diff = dr.difficulty; + const total = + dr.difficulty !== "All" + ? allQuestions.filter( + (qn) => mapRangeToDifficulty(qn.difficulty) === diff + ).length + : allQuestions.length; + const solved = + dr.difficulty !== "All" + ? new Set( + solvedQuestions + .filter( + (qn) => + qn.solved && + mapRangeToDifficulty(qn.difficulty) === diff + ) + .map((q) => q._id) + ).size + : new Set( + solvedQuestions.filter((qn) => qn.solved).map((q) => q._id) + ).size; + return { + name: diff, + percentage: total + ? Math.round((solved / total) * 10000) / 100 + : 0.0, + solved, + total, + scheme: dr.scheme, + }; + }); + setDatas(res); } catch (error) { console.error(error); } - } - - fetchAndCategorizeQuestions(); + })(); }, [solvedQuestions]); - + console.log(solvedQuestions); return ( <> - - - - -
- Solved Problems -
-
- - - - {totalSolved} solved - - - - Easy - - {easyCount}/{totalEasy} - - - - - Medium - - {mediumCount}/{totalMedium} - - - - - Hard - - {hardCount}/{totalHard} - - - - - - + + Solved Problems + + + {datas + .filter((d) => d.name === "All") + .map((d) => ( + + + + Total Solved + + {d.solved}/{d.total} + + {d.percentage}% + + + + ))} + + {datas + .filter((d) => d.name !== "All") + .map((d) => ( + + + {d.name} + + {d.solved}/{d.total} + + + + + ))} + + ); diff --git a/frontend/src/components/QnDrawer/QnDrawer.component.tsx b/frontend/src/components/QnDrawer/QnDrawer.component.tsx index 6a52341c..ca5c02ab 100644 --- a/frontend/src/components/QnDrawer/QnDrawer.component.tsx +++ b/frontend/src/components/QnDrawer/QnDrawer.component.tsx @@ -14,8 +14,8 @@ import { } from "@chakra-ui/react"; import { Question } from "../../models/Question.model"; import { ArrowRightIcon } from "@chakra-ui/icons"; -import { diffToScheme } from "../../helper/UIHelper"; import { MarkdownViewer } from "../MarkdownVIewer/MarkdownViewer"; +import { rangeToScheme } from "../../helper/DifficultyFilterHelper"; interface qnProp { question: Question; @@ -47,7 +47,7 @@ export const QnDrawer = (prop: qnProp) => { {question.displayedQuestion} - + Difficulty: {question.difficulty} diff --git a/frontend/src/components/QnFilter/DifficultyFilter.component.tsx b/frontend/src/components/QnFilter/DifficultyFilter.component.tsx index 354e8c82..6fca496a 100644 --- a/frontend/src/components/QnFilter/DifficultyFilter.component.tsx +++ b/frontend/src/components/QnFilter/DifficultyFilter.component.tsx @@ -1,21 +1,23 @@ import React from "react"; import { Flex, Select } from "@chakra-ui/react"; import { QnFilter } from "../../models/Question.model"; -import { mapDifficultyToRange, mapRangeToDifficulty} from "../../helper/DifficultyFilterHelper" +import { + diffRanges, + difficultyToRange, +} from "../../helper/DifficultyFilterHelper"; export interface DifficultyFilterProps extends QnFilter { setDifficultyFilter: (difficultyFilter: [number, number]) => void; } export const DifficultyFilter = ({ - difficultyFilter = [0, 10], setDifficultyFilter, }: DifficultyFilterProps) => { const handleDifficultyChange = ( event: React.ChangeEvent ) => { const difficulty = event.target.value; - const newDifficultyFilter = mapDifficultyToRange(difficulty); + const newDifficultyFilter = difficultyToRange(difficulty); setDifficultyFilter(newDifficultyFilter); }; @@ -24,13 +26,16 @@ export const DifficultyFilter = ({ ); diff --git a/frontend/src/components/QnSubmissionHistory/QnSubmissionHistory.component.tsx b/frontend/src/components/QnSubmissionHistory/QnSubmissionHistory.component.tsx index aaf1889a..8733feea 100644 --- a/frontend/src/components/QnSubmissionHistory/QnSubmissionHistory.component.tsx +++ b/frontend/src/components/QnSubmissionHistory/QnSubmissionHistory.component.tsx @@ -113,9 +113,6 @@ const QnSubmissionHistory = () => { - {submissions.map((s, i) => ( - - ))} {currSubmission ? ( { ) : ( <> )} + {submissions.map((s, i) => ( + + ))} diff --git a/frontend/src/components/QnTable/QnTable.component.tsx b/frontend/src/components/QnTable/QnTable.component.tsx index e4b55b5c..745c8a82 100644 --- a/frontend/src/components/QnTable/QnTable.component.tsx +++ b/frontend/src/components/QnTable/QnTable.component.tsx @@ -43,7 +43,7 @@ export function QnTable(pp: TableProp) {
- - {isAdmin ? : <>} + + {isAdmin ? : <>} - {getCurrentPage(pageNumber).map((qn) => QuestionEntry({qn: qn, isAdmin: isAdmin}))} + + {getCurrentPage(pageNumber).map((qn) => + QuestionEntry({ qn: qn, isAdmin: isAdmin }) + )} +
Questions TypeDifficultyModify/Delete +
Difficulty
+
Modify/Delete
diff --git a/frontend/src/components/QnTable/QuestionEntry.component.tsx b/frontend/src/components/QnTable/QuestionEntry.component.tsx index 621a4820..add6df88 100644 --- a/frontend/src/components/QnTable/QuestionEntry.component.tsx +++ b/frontend/src/components/QnTable/QuestionEntry.component.tsx @@ -1,63 +1,74 @@ -import { ButtonGroup, HStack, IconButton, Tag, Td, Text, Tr } from "@chakra-ui/react"; +import { + ButtonGroup, + Center, + HStack, + IconButton, + Tag, + Td, + Text, + Tr, +} from "@chakra-ui/react"; import { Question } from "../../models/Question.model"; import { Link } from "react-router-dom"; import { DeleteQnBtn } from "../DeleteQnBtn/DeleteQnBtn.component"; import { EditIcon } from "@chakra-ui/icons"; -import { diffToScheme } from "../../helper/UIHelper"; import { useDispatch } from "react-redux"; import { delQuestion } from "../../api/questions"; import { deleteQuestion } from "../../reducers/questionsSlice"; +import { rangeToScheme } from "../../helper/DifficultyFilterHelper"; interface QuestionEntryProps { - qn: Question, - isAdmin: boolean - } + qn: Question; + isAdmin: boolean; +} -export function QuestionEntry(props : QuestionEntryProps) { - - const {qn, isAdmin} = props; - const dispatch = useDispatch(); +export function QuestionEntry(props: QuestionEntryProps) { + const { qn, isAdmin } = props; + const dispatch = useDispatch(); - const onDelete = async (_id: string) => { - await delQuestion(_id); - dispatch(deleteQuestion(_id)); - } + const onDelete = async (_id: string) => { + await delQuestion(_id); + dispatch(deleteQuestion(_id)); + }; - return ( - - - - {qn.displayedQuestion} - + return ( + + + + {qn.displayedQuestion} + + + + + {qn.topics.map((qntype) => ( + {qntype} + ))} + + + +
+ {qn.difficulty} +
+ + {isAdmin ? ( + + + + } + colorScheme="blue" + > + + onDelete(qn._id)} + > + - - - {qn.topics.map((qntype) => ( - {qntype} - ))} - - - - {qn.difficulty} - - {isAdmin ? ( - - - - } - > - - onDelete(qn._id)} - > - - - ) : ( - <> - )} - - ); - }; \ No newline at end of file + ) : ( + <> + )} + + ); +} diff --git a/frontend/src/components/QuestionEditor/QuestionEditor.component.tsx b/frontend/src/components/QuestionEditor/QuestionEditor.component.tsx index d2926e60..81fa05ef 100644 --- a/frontend/src/components/QuestionEditor/QuestionEditor.component.tsx +++ b/frontend/src/components/QuestionEditor/QuestionEditor.component.tsx @@ -22,9 +22,9 @@ import { } from "@chakra-ui/react"; import { MarkdownEditor } from "../MarkdownEditor/MarkdownEditor.component"; import { CloseableTag } from "../CloseableTag/CloseableTag.component"; -import { diffToScheme } from "../../helper/UIHelper"; import { CheckIcon, CloseIcon } from "@chakra-ui/icons"; import { MarkdownViewer } from "../MarkdownVIewer/MarkdownViewer"; +import { rangeToScheme } from "../../helper/DifficultyFilterHelper"; type QuestionEditorProp = { question?: Question; @@ -37,7 +37,7 @@ export const QuestionEditor = (prop: QuestionEditorProp) => { question, onSubmit = async (x) => { await new Promise((resolve) => setTimeout(resolve, 1000)); - throw Error('no action defined for Question Editor onSubmit') + throw Error("no action defined for Question Editor onSubmit"); }, } = prop; @@ -89,7 +89,7 @@ export const QuestionEditor = (prop: QuestionEditorProp) => { const buildQuestion = () => { return new Question( - question?._id ?? '', + question?._id ?? "", question?.id ?? -1, title, description, @@ -114,14 +114,14 @@ export const QuestionEditor = (prop: QuestionEditorProp) => { try { await onSubmit(buildQuestion(), testCasesFile); console.log("Submit success!"); - } catch (err : any) { + } catch (err: any) { toast({ title: "Creation/Modification of question has failed!", description: err.message, status: "error", isClosable: false, }); - } + } setIsSubmitting(false); }; @@ -165,7 +165,7 @@ export const QuestionEditor = (prop: QuestionEditorProp) => { Difficulty: - + {difficulty} @@ -208,11 +208,18 @@ export const QuestionEditor = (prop: QuestionEditorProp) => { Testcases - - + " + /> + diff --git a/frontend/src/components/SolvedTable/SolvedTable.component.tsx b/frontend/src/components/SolvedTable/SolvedTable.component.tsx index 6714629d..88b8bceb 100644 --- a/frontend/src/components/SolvedTable/SolvedTable.component.tsx +++ b/frontend/src/components/SolvedTable/SolvedTable.component.tsx @@ -9,9 +9,19 @@ import { TableCaption, Center, TableContainer, + HStack, + Tag, + Heading, + IconButton, + Text, + Flex, + Box, + Tooltip, } from "@chakra-ui/react"; import { Paginator } from "../Paginator/Paginator.component"; import { useProfile } from "../../contexts/profileContext"; +import { CheckIcon, CloseIcon } from "@chakra-ui/icons"; +import { rangeToScheme } from "../../helper/DifficultyFilterHelper"; export type TableProp = { pageSize?: number; @@ -26,14 +36,20 @@ export const SolvedTable = (props: TableProp) => { setCurrentPage(page); }; - const getQuestionsForPage = () => { const startIndex = (currentPage - 1) * pageSize; return solvedQuestions.slice(startIndex, startIndex + pageSize); }; + if (solvedQuestions.length === 0) + return ( +
+ You have no past submissions! +
+ ); + return ( - + { - + - - + + + - {getQuestionsForPage().map((question) => ( - + {getQuestionsForPage().map((question, i) => ( + - - - + + + ))} diff --git a/frontend/src/components/profile_page/PromoteAdminCard/PromoteAdminCard.component.tsx b/frontend/src/components/profile_page/PromoteAdminCard/PromoteAdminCard.component.tsx index cbd15fe4..71ec0563 100644 --- a/frontend/src/components/profile_page/PromoteAdminCard/PromoteAdminCard.component.tsx +++ b/frontend/src/components/profile_page/PromoteAdminCard/PromoteAdminCard.component.tsx @@ -15,33 +15,33 @@ export default function PromoteAdminCard(props: PromoteAdminCardProp) { const promoteToAdmin = async () => { try { - const response = await updateUserRole(parseInt(displayedUser.id), 'ADMIN'); + const response = await updateUserRole(displayedUser.id, "ADMIN"); const updatedDisplayedUser = response.data; - console.log(`setting new displayed user to: ${updatedDisplayedUser}`) + console.log(`setting new displayed user to: ${updatedDisplayedUser}`); setDisplayedUser(updatedDisplayedUser); - } catch (error : any) { + } catch (error: any) { toast({ - title: 'Failed to promote', + title: "Failed to promote", description: error.message, - status: 'error' - }) + status: "error", + }); } - } + }; const demoteToUser = async () => { try { - const response = await updateUserRole(parseInt(displayedUser.id), 'USER'); + const response = await updateUserRole(displayedUser.id, "USER"); const updatedDisplayedUser = response.data; - console.log(`setting new displayed user to: ${updatedDisplayedUser}`) + console.log(`setting new displayed user to: ${updatedDisplayedUser}`); setDisplayedUser(updatedDisplayedUser); - } catch (error : any) { + } catch (error: any) { toast({ - title: 'Failed to demote', + title: "Failed to demote", description: error.message, - status: 'error' - }) + status: "error", + }); } - } + }; return ( diff --git a/frontend/src/contexts/profileContext.tsx b/frontend/src/contexts/profileContext.tsx index 7fe0c200..297b0fbd 100644 --- a/frontend/src/contexts/profileContext.tsx +++ b/frontend/src/contexts/profileContext.tsx @@ -29,6 +29,7 @@ export function ProfileProvider({ displayedUser, children }: ProfileProviderProp if (displayedUser?.id) { try { const data = await fetchUserCompletedQuestions(displayedUser?.id); + data.reverse(); // sort from latest to oldest setSolvedQuestions(data); } catch (error) { console.error("Failed to fetch solved questions:", error); diff --git a/frontend/src/contexts/sharededitor.context.tsx b/frontend/src/contexts/sharededitor.context.tsx index 5300f0b0..3e557272 100644 --- a/frontend/src/contexts/sharededitor.context.tsx +++ b/frontend/src/contexts/sharededitor.context.tsx @@ -19,7 +19,7 @@ import data from "../data/lang_temps.json"; import { ToastId, useToast } from "@chakra-ui/react"; import { wsCollabUrl } from "../api/gateway"; import { getExecutionResult, submitCodeForExecution } from "../api/code"; -import { getProfilePicUrl } from "../api/user"; +import { fetchUserCompletedQuestions, getProfilePicUrl } from "../api/user"; export type language = keyof typeof data; @@ -50,7 +50,7 @@ export type SubmissionResult = { export type submissionRecord = { time: number; - user: string; + userId: number; qn_id: string; code: string; lang: language; @@ -156,7 +156,7 @@ export const SharedEditorProvider = ({ lang: submission.lang, source_code: submission.code, qn__id: submission.qn_id, - uid: submission.user, + uid: submission.userId, }); const token = res.data.token as string; @@ -200,7 +200,7 @@ export const SharedEditorProvider = ({ if (!state || currSubmission || !lang || !ycode) return; const tmp: submissionRecord = { time: Date.now(), - user: user.username, + userId: user.id, code: ycode.toString(), lang: lang, qn_id: qn?._id ?? "-1", // in case we implement a sandbox code editor @@ -276,7 +276,7 @@ export const SharedEditorProvider = ({ (ystates.get(SUBMISSION_STATE) as submissionRecord) ?? null; setCurrSubmission(newSubmission); // if react changes are propageted in the next cycle. - if (newSubmission && newSubmission.user !== user.username) { + if (newSubmission && newSubmission.userId !== user.id) { if (!matchedRoom || matchedRoom.isMaster) { // if a master receive it if (!currSubmission) { @@ -456,17 +456,20 @@ export const SharedEditorProvider = ({ setSubmissionLoading(false); return; } - await new Promise((r) => setTimeout(r, 6000)); // simulate fetching submission history - cachedPastSubmissions.current = [ - { - time: Date.now(), - user: user.username, - code: "lorem ipsum", - lang: "c++17", - qn_id: "1", - result: "TLE", - }, - ]; + cachedPastSubmissions.current = ( + await fetchUserCompletedQuestions(user.id) + ) + .filter((_qn) => { + return _qn._id === qn._id; + }) + .map((_qn) => { + return { + code: _qn.sourceCode, + lang: _qn.language, + qn_id: _qn._id, + result: _qn.verdict, + } as submissionRecord; + }); setSubmissions( cachedPastSubmissions.current.concat(ysubmissions.toArray()) ); // updates submission array diff --git a/frontend/src/helper/DifficultyFilterHelper.tsx b/frontend/src/helper/DifficultyFilterHelper.tsx index f908360a..f82e9142 100644 --- a/frontend/src/helper/DifficultyFilterHelper.tsx +++ b/frontend/src/helper/DifficultyFilterHelper.tsx @@ -1,25 +1,42 @@ -export const mapDifficultyToRange = (difficulty: string): [number, number] => { - switch (difficulty) { - case "Easy": - return [0, 3.5]; - case "Medium": - return [3.5, 7]; - case "Hard": - return [7, 10]; - default: - return [0, 10]; - } - }; +type difficulty = { + difficulty: string; + range: [number, number]; + scheme?: string; +}; -export const mapRangeToDifficulty = (difficultyFilter: [number, number]): string => { - const [start, end] = difficultyFilter; - if (start === 0 && end === 3.5) { - return "Easy"; - } else if (start === 3.5 && end === 7) { - return "Medium"; - } else if (start === 7 && end === 10) { - return "Hard"; - } else { - return ""; - } - }; \ No newline at end of file +export const diffRanges: difficulty[] = [ + { + difficulty: "Basic", + range: [0, 2], + scheme: "green", + }, + { + difficulty: "Easy", + range: [2.001, 4], + scheme: "yellow", + }, + { + difficulty: "Medium", + range: [4.001, 6.5], + scheme: "orange", + }, + { + difficulty: "Hard", + range: [6.501, 10], + scheme: "red", + }, + { + difficulty: "All", + range: [0, 10], + }, +]; + +export const rangeToScheme = (diff: number) => + diffRanges.find((dr) => dr.range[0] <= diff && dr.range[1] >= diff)?.scheme; + +export const mapRangeToDifficulty = (diff: number) => + diffRanges.find((dr) => dr.range[0] <= diff && dr.range[1] >= diff) + ?.difficulty ?? ""; + +export const difficultyToRange = (diff: string) => + diffRanges.find((dr) => dr.difficulty === diff)?.range ?? [0, 10]; diff --git a/frontend/src/helper/UIHelper.tsx b/frontend/src/helper/UIHelper.tsx deleted file mode 100644 index 444e6d21..00000000 --- a/frontend/src/helper/UIHelper.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/// maps difficulty value to appropriate colors -export const diffToScheme = (diff: number) => { - if (diff < 3) { - return "green"; - } - if (diff < 6) { - return "yellow"; - } - if (diff < 8) { - return "orange"; - } - return "red"; -}; diff --git a/frontend/src/models/SolvedQuestion.model.tsx b/frontend/src/models/SolvedQuestion.model.tsx index 5df7aa13..95023bdb 100644 --- a/frontend/src/models/SolvedQuestion.model.tsx +++ b/frontend/src/models/SolvedQuestion.model.tsx @@ -2,7 +2,10 @@ import { Question, QnFilter } from "./Question.model"; export class SolvedQuestion extends Question { solved: boolean; - solvedDate: Date | undefined; + verdict: string; + sourceCode: string; + language: string; + solvedDate: Date | undefined; constructor( _id: string, @@ -11,12 +14,17 @@ export class SolvedQuestion extends Question { descMd: string, categories: string[], difficulty: number, - solved: boolean = false, - solvedDate?: Date + verdict: string, + sourceCode: string, + language: string, + solvedDate?: Date ) { super(_id, id, title, descMd, categories, difficulty); - this.solved = solved; + this.solved = verdict === "Accepted"; + this.verdict = verdict; this.solvedDate = solvedDate; + this.sourceCode = sourceCode; + this.language = language; } } diff --git a/frontend/src/pages/BankPage/Bank.page.tsx b/frontend/src/pages/BankPage/Bank.page.tsx index c4e76cc3..070776a7 100644 --- a/frontend/src/pages/BankPage/Bank.page.tsx +++ b/frontend/src/pages/BankPage/Bank.page.tsx @@ -44,10 +44,7 @@ const BankPage = () => { qnFilter={filter.qnFilter} setFilter={(newFilter) => setFilter(newFilter)} /> - setDifficultyFilter(newDifficultyFilter)} - /> + diff --git a/frontend/src/pages/HomePage/HomePage.tsx b/frontend/src/pages/HomePage/HomePage.tsx index 9b59c055..4111c765 100644 --- a/frontend/src/pages/HomePage/HomePage.tsx +++ b/frontend/src/pages/HomePage/HomePage.tsx @@ -7,6 +7,7 @@ import { SolvedTable } from "../../components/SolvedTable/SolvedTable.component" import { selectUser } from "../../reducers/authSlice"; import { useSelector } from "react-redux"; import { ProfileProvider } from "../../contexts/profileContext"; +import { Box } from "@chakra-ui/react"; function HomePage() { // Use the useSelector hook to access the user from the Redux store @@ -24,8 +25,10 @@ function HomePage() { return (
- - + + + +
); diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index b43de96d..26b625e7 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -43,27 +43,28 @@ export default function ProfilePage() { /> - { - currUser!.id !== displayedUser!.id - ? <> - : - } - - { - isAdmin - ? - : <> - } + {currUser!.id !== displayedUser!.id ? ( + <> + ) : ( + + )} + {isAdmin ? ( + + ) : ( + <> + )} - + - - + + - - ) + ); } \ No newline at end of file diff --git a/frontend/src/reducers/authSlice.ts b/frontend/src/reducers/authSlice.ts index b747cbde..b494267e 100644 --- a/frontend/src/reducers/authSlice.ts +++ b/frontend/src/reducers/authSlice.ts @@ -2,32 +2,32 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from './store'; interface User { - id: string; - username: string; - role: 'ADMIN' | 'USER'; - bio: string | null; - profilePic: string | null; -}; + id: number; + username: string; + role: "ADMIN" | "USER"; + bio: string | null; + profilePic: string | null; +} interface AuthState { - user: User | null; -}; + user: User | null; +} const dummyUser: User = { - id: '1', - username: 'Harro_world', - role: 'USER', - bio: null, - profilePic: null, -} + id: 1, + username: "Harro_world", + role: "USER", + bio: null, + profilePic: null, +}; const dummyAdmin: User = { - id: '21', - username: 'admin', - role: 'ADMIN', - bio: null, - profilePic: null, -} + id: 21, + username: "admin", + role: "ADMIN", + bio: null, + profilePic: null, +}; const initialState: AuthState = { user: dummyUser // null diff --git a/user_service/prisma/schema.prisma b/user_service/prisma/schema.prisma index 92edbe20..6a500c80 100644 --- a/user_service/prisma/schema.prisma +++ b/user_service/prisma/schema.prisma @@ -2,7 +2,7 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" binaryTargets = ["native", "debian-openssl-3.0.x"] } @@ -17,25 +17,28 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - username String @db.VarChar(255) @unique - profilePic String? - bio String? - hashedPassword String - role Role @default(USER) + id Int @id @default(autoincrement()) + username String @unique @db.VarChar(255) + profilePic String? + bio String? + hashedPassword String + role Role @default(USER) answeredQuestions AnsweredQuestion[] } - model AnsweredQuestion { - id String @id @default(uuid()) - userId Int - user User @relation(fields: [userId], references: [id]) + id String @id @default(uuid()) + userId Int + user User @relation(fields: [userId], references: [id]) questionTitle String - questionId Int - difficulty Float - topics String[] - solved Boolean - answeredAt DateTime @default(now()) + questionId Int + question__id String + difficulty Float + topics String[] + sourceCode String + language String + verdict String + answeredAt DateTime @default(now()) + @@map("AnsweredQuestion") -} \ No newline at end of file +} diff --git a/user_service/src/controllers/UserController.ts b/user_service/src/controllers/UserController.ts index 6e3af25f..b70058a6 100644 --- a/user_service/src/controllers/UserController.ts +++ b/user_service/src/controllers/UserController.ts @@ -91,16 +91,31 @@ export async function getUserQuestions(req: any, res: any) { //@access authenticated users export async function addUserQuestion(req: Request, res: Response) { try { - const { userId, questionTitle, questionId, difficulty, topics, solved} = req.body; + const { + question__id, + userId, + questionTitle, + questionId, + difficulty, + topics, + verdict, + sourceCode, + language, + answeredAt, + } = req.body; const createdQuestion = await prisma.answeredQuestion.create({ data: { - userId: userId, - questionTitle: questionTitle, - questionId: questionId, - difficulty: difficulty, + question__id, + userId, + questionTitle, + questionId, + difficulty, + verdict, + sourceCode, + language, + answeredAt, topics: { set: topics }, - solved: solved, }, });
Completed QuestionsRecent Submissions
Question TopicsDifficultySolved Date +
Difficulty
+
+
Submission Date
+
+
Verdict
+
{question.title}{question.topics.join(", ")}{question.difficulty} - {question.solvedDate - ? new Date(question.solvedDate).toLocaleDateString() - : "Not Available"} + + + {question.topics.map((qntype) => ( + {qntype} + ))} + + +
+ + {question.difficulty} + +
+
+
+ {question.solvedDate + ? new Date(question.solvedDate).toLocaleDateString() + : "Not Available"} +
+
+ +
+ : } + _hover={{}} + _active={{}} // disables click/hover effect + /> +
+