From 69cbe5dc42d954337bbf7591d954fd588c72b3a3 Mon Sep 17 00:00:00 2001 From: Nishant Date: Wed, 8 Jan 2025 00:28:42 +0530 Subject: [PATCH 1/8] fix: Fixed the bug of unverified user shown on leaderboard --- server-side/controllers/clientControllers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server-side/controllers/clientControllers.js b/server-side/controllers/clientControllers.js index 99b0d5b..a625ac9 100644 --- a/server-side/controllers/clientControllers.js +++ b/server-side/controllers/clientControllers.js @@ -56,9 +56,9 @@ module.exports.leaderboard = async (req, res, next) => { return res.status(401).json({ status: false, msg: "Session expired or invalid" }); } - const cfID = await Users.find().select(["cfID"]); + const cfID = await Users.find({ emailVerified: true, cfVerified: true }).select(["cfID"]); return res.json({ status: true, data: cfID }); - } + } catch (error) { next(error); } From a45dca22549747f99f07f57a2b8831e31eff6b41 Mon Sep 17 00:00:00 2001 From: Nishant Date: Wed, 8 Jan 2025 00:29:14 +0530 Subject: [PATCH 2/8] style: Change the leaderboard styling with some proper ranking change --- .../src/pages/Leaderboard/LeaderUser.jsx | 81 ++++++++ .../src/pages/Leaderboard/Leaderboard.css | 91 +++++---- .../src/pages/Leaderboard/Leaderboard.jsx | 179 ++++++++---------- 3 files changed, 207 insertions(+), 144 deletions(-) create mode 100644 client-side/src/pages/Leaderboard/LeaderUser.jsx diff --git a/client-side/src/pages/Leaderboard/LeaderUser.jsx b/client-side/src/pages/Leaderboard/LeaderUser.jsx new file mode 100644 index 0000000..3c387cd --- /dev/null +++ b/client-side/src/pages/Leaderboard/LeaderUser.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import './Leaderboard.css'; // Ensure this includes styles for user avatars, ranks, etc. +import { useNavigate } from 'react-router-dom'; +function LeaderUser(props) { + const { avatar, name, rank, rating, positionChange, title, cfID } = props; + const navigate = useNavigate(); + // Compare rating with the leaderboard + // Rank color based on Codeforces rank + const rankColor = getRankColor(title); + + // Handle user click to navigate to the user's Codeforces profile + const handleUserClick = () => { + navigate(`/get-codeforces-profile/${cfID}`); + }; + const handleCodeforcesRedirect = () => { + window.open(`https://codeforces.com/profile/${cfID}`, "_blank"); + }; + + return ( +
+
+ {`${name} + + #{rank} {name} + + +
+ + +
+
+ {positionChange === 0 && ( + + 0 + + )} + {positionChange > 0 && ( + + +{positionChange} + + )} + {positionChange < 0 && ( + + {positionChange} + + )} +
+
+
+ ); +} + +function getRankColor(rank) { + switch(rank) { + case 'newbie': return 'gray'; + case 'pupil': return 'green'; + case 'specialist': return 'cyan'; + case 'expert': return 'blue'; + case 'candidate master': return 'purple'; + case 'master': return 'orange'; + case 'international master': return 'red'; + case 'grandmaster': return 'darkred'; + case 'international grandmaster': return 'darkred'; + case 'legendary grandmaster': return 'darkred'; + default: return 'black'; + } +} + +export default LeaderUser; diff --git a/client-side/src/pages/Leaderboard/Leaderboard.css b/client-side/src/pages/Leaderboard/Leaderboard.css index 3ebdcc0..0e5f4e0 100644 --- a/client-side/src/pages/Leaderboard/Leaderboard.css +++ b/client-side/src/pages/Leaderboard/Leaderboard.css @@ -1,56 +1,55 @@ -.leader-heading { - font-size: xx-large; - color: #F94479; - text-align: center; - padding: 20px; -} .leader-box { - padding: 10px; - background-color: white; - color: rgb(39, 8, 82); - text-align: center; - margin-left: 20px; - margin-right: 20px; - border: 0.5px solid rgb(39, 8, 82); display: flex; - flex-direction: row; justify-content: space-between; + padding: 10px; + border: 1px solid #ddd; + margin: 5px 0; + border-radius: 5px; + background-color: #f7f7f7; cursor: pointer; + transition: background-color 0.3s ease; +} + +.leader-box:hover { + background-color: #e0e0e0; +} + +.leader-info { + display: flex; + align-items: center; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 10px; } -.leader-box:hover{ - background-color: rgb(234, 227, 252); +.leader-rank { + font-size: 16px; + font-weight: bold; +} +.leader-rating { + display: flex; + align-items: center; } -.leader-title { +.leader-rating-value { + font-size: 18px; font-weight: bold; - width: 100%; - font-size: larger; - text-align: left; -} -.leader-date { - width: 100%; - text-align: left; - font-size: smaller; - margin-bottom: 5px; -} -.leader-body { - text-align: justify; - width: 100%; -} -pre { - white-space: pre-wrap; - } -.leader-footer { - text-align: center; - font-size: small; - margin-left: 20px; - margin-right: 20px; -} - -@media screen and (max-width: 480px) { - .leader-reg{ - display: none; - } -} \ No newline at end of file + margin-right: 10px; +} + +.rating-change { + font-size: 16px; +} + +.positive { + color: green; +} + +.negative { + color: red; +} diff --git a/client-side/src/pages/Leaderboard/Leaderboard.jsx b/client-side/src/pages/Leaderboard/Leaderboard.jsx index a084cfe..c145097 100644 --- a/client-side/src/pages/Leaderboard/Leaderboard.jsx +++ b/client-side/src/pages/Leaderboard/Leaderboard.jsx @@ -5,36 +5,9 @@ import axios from 'axios'; import NavSpace from '../../components/NavSpace'; import Spinner from '../../components/Spinner/Spinner'; import Alert from '../../components/Alert/Alert'; -// import NavBarSecond from '../../components/NavBar/NavBarSecond'; import Footer from '../../components/Footer/Footer'; -import { useNavigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; - -function LeaderUser(props) { - const navigate = useNavigate() - function handleUserClick(){ - navigate(`/get-codeforces-profile/${props.name}`) - } - - return ( -
-
handleUserClick()}> -
- - - #{props.rank} - - - {props.name} - -
- {/*
- {props.regNo} -
*/} -
-
- ); -} +import LeaderUser from './LeaderUser'; export default function Leaderboard() { const { user } = useSelector((state) => state.auth); @@ -42,77 +15,85 @@ export default function Leaderboard() { ); - - async function SortUsersByRating(userBoardInfo){ - //List of all the Codeforces contests - const contests = await axios.get('https://codeforces.com/api/contest.list') - const contests_data = contests.data.result; - - var latestContestId; - for(const contest of contests_data){ - if(contest.phase === "FINISHED"){ - latestContestId = contest.id; - break; - } - } - - //Users who have given latest contest - const users = await axios.get(`https://codeforces.com/api/contest.ratingChanges?contestId=${latestContestId}`) - const userRatings = []; - - - for (const userInfo of userBoardInfo) { - var user = users.data.result.find((usr)=>usr.handle === userInfo.cfID) - if(!user){ - userRatings.push({handle:userInfo.cfID, rating: "N/A"}) - } - else{ - userRatings.push({handle:user.handle, rating: user.newRating}) - } - } - - - // Sort the users by rating in decreasing order - userBoardInfo.sort((a, b) => { - const aRating = userRatings.find((user) => user.handle === a.cfID).rating; - const bRating = userRatings.find((user) => user.handle === b.cfID).rating; - if (aRating === "N/A" && bRating === "N/A") { - return 0; - } else if (aRating === "N/A") { - return 1; - } else if (bRating === "N/A") { - return -1; - } else { - return bRating - aRating; - } - }); -} + // Function to sort users by their ratings (new or usual) + const sortUsersByRating = (userBoardInfo, userRatingsKey) => { + return [...userBoardInfo].sort((a, b) => { + const aRating = a[userRatingsKey] || 0; + const bRating = b[userRatingsKey] || 0; + return bRating - aRating; // Sort in descending order + }); + }; const updatePageHtml = async () => { - try { + const leaderboardAPIResponse = await axios.post( + `${process.env.REACT_APP_SERVER_BASE_URL}/leaderboard`, + { cfID: user.cfID }, + { withCredentials: true } + ); - // const user = await JSON.parse(localStorage.getItem(process.env.CODETOGETHER_APP_LOCALHOST_KEY)); - const LeaderboardAPIresponse = await axios.post(process.env.REACT_APP_SERVER_BASE_URL + '/leaderboard', { cfID: user.cfID }, { withCredentials: true }); - const userBoardInfo = LeaderboardAPIresponse.data.data; - console.log(userBoardInfo) - await SortUsersByRating(userBoardInfo) - const LeaderComponent = userBoardInfo.map((userInfo, index) => ); + const userBoardInfo = leaderboardAPIResponse.data.data; + + // Get the latest contest ID + const contests = await axios.get('https://codeforces.com/api/contest.list'); + const latestContest = contests.data.result.find((contest) => contest.phase === 'FINISHED'); + const latestContestId = latestContest.id; + + // Get ratings from the latest contest + const ratingChanges = await axios.get(`https://codeforces.com/api/contest.ratingChanges?contestId=${latestContestId}`); + const participants = ratingChanges.data.result; + console.log(participants); + // Fetch user info for usual ratings + const userRatings = await Promise.all( + userBoardInfo.map(async (user) => { + const userInfo = await axios.get(`https://codeforces.com/api/user.info?handles=${user.cfID}`); + const userDetails = userInfo.data.result[0]; + console.log(userDetails); + return { + cfID: user.cfID, + avatar:userDetails.avatar, + rank:userDetails.rank, + usualRating: userDetails.rating || 0, + newRating: participants.find((p) => p.handle === user.cfID)?.newRating || 0, + }; + }) + ); + // Sort users by new ratings and usual ratings + userRatings.sort((a, b) => b.usualRating - a.usualRating); + const sortedByNewRating = sortUsersByRating(userRatings, 'newRating'); + const sortedByUsualRating = sortUsersByRating(userRatings, 'usualRating'); + + // Calculate position changes + const leaderComponents = sortedByNewRating.map((user, index) => { + const oldPosition = sortedByUsualRating.findIndex((u) => u.cfID === user.cfID) + 1; + const newPosition = index + 1; + const positionChange = oldPosition-newPosition; + console.log(user); + return ( + + ); + }); setPageHtml(<>
- {/* */} -
Leaderboard
-

Inactive participant's ratings are considered 0(zero).

-
- {LeaderComponent} -
+
Leaderboard
+

+ Rankings are based on the latest contest results. Ratings of inactive participants are considered 0. +

+
{leaderComponents}
@@ -120,24 +101,26 @@ export default function Leaderboard() { } catch (err) { setPageHtml( <> - {/* */} -
- +
+
); } - } + }; useEffect(() => { updatePageHtml(); }, []); - - return ( - <>{PageHtml} - ); + return <>{PageHtml}; } From 70050da80992d4a779921c3d23b9c6205eae2511 Mon Sep 17 00:00:00 2001 From: Nishant Date: Wed, 8 Jan 2025 00:40:23 +0530 Subject: [PATCH 3/8] style: Add rank position --- client-side/src/pages/Leaderboard/LeaderUser.jsx | 4 ++-- client-side/src/pages/Leaderboard/Leaderboard.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client-side/src/pages/Leaderboard/LeaderUser.jsx b/client-side/src/pages/Leaderboard/LeaderUser.jsx index 3c387cd..7652f14 100644 --- a/client-side/src/pages/Leaderboard/LeaderUser.jsx +++ b/client-side/src/pages/Leaderboard/LeaderUser.jsx @@ -20,8 +20,8 @@ function LeaderUser(props) {
{`${name} - - #{rank} {name} + #{rank} + {name}
diff --git a/client-side/src/pages/Leaderboard/Leaderboard.jsx b/client-side/src/pages/Leaderboard/Leaderboard.jsx index c145097..ced1c28 100644 --- a/client-side/src/pages/Leaderboard/Leaderboard.jsx +++ b/client-side/src/pages/Leaderboard/Leaderboard.jsx @@ -74,7 +74,7 @@ export default function Leaderboard() { return ( Date: Wed, 8 Jan 2025 01:49:14 +0530 Subject: [PATCH 4/8] fix: Invalid User profile detail shown handled --- client-side/src/pages/DashBoard/dashboard.jsx | 16 ++++++++++++++-- .../controllers/Client/auth/checkUser.js | 19 +++++++++++++++++++ server-side/controllers/Client/controller.js | 4 +++- server-side/routes/clientRoutes.js | 12 ++++++++---- 4 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 server-side/controllers/Client/auth/checkUser.js diff --git a/client-side/src/pages/DashBoard/dashboard.jsx b/client-side/src/pages/DashBoard/dashboard.jsx index e7dc058..e68b8bb 100644 --- a/client-side/src/pages/DashBoard/dashboard.jsx +++ b/client-side/src/pages/DashBoard/dashboard.jsx @@ -38,8 +38,18 @@ export default function UserHome() { if (id) { cfID = id; } - console.log(cfID); - + const redirect = async (cfID) => { + try{ + const response = await axios.get(`${process.env.REACT_APP_SERVER_BASE_URL}/check/user/${cfID}`,{withCredentials: true}); + if(response.data.status === false){ + navigate("/"); + } + }catch(error){ + console.log(error); + navigate("/"); + } + + } let userData = { status: "", data: {} }; let userRating = { status: "", data: {} }; let userSubmissions = { status: "", data: {} }; @@ -100,6 +110,7 @@ export default function UserHome() { // API calls and Initialisation of Data Members try { console.log(cfID); + const userDataAPI = await axios.get("https://codeforces.com/api/user.info?handles=" + cfID); const userRatingAPI = await axios.get("https://codeforces.com/api/user.rating?handle=" + cfID); const userSubmissionsAPI = await axios.get("https://codeforces.com/api/user.status?handle=" + cfID); @@ -244,6 +255,7 @@ export default function UserHome() { } useEffect(() => { + redirect(cfID); fetchData(); }, [cfID]); diff --git a/server-side/controllers/Client/auth/checkUser.js b/server-side/controllers/Client/auth/checkUser.js new file mode 100644 index 0000000..8be8021 --- /dev/null +++ b/server-side/controllers/Client/auth/checkUser.js @@ -0,0 +1,19 @@ +const User = require("../../../model/userModel"); +const AsyncErrorHandler = require("../../../ErrorHandlers/async_error_handler"); + +module.exports = AsyncErrorHandler(async (req, res, next) => { + const { id } = req.params; + try { + const user = await User.findOne({ cfID: id }); + if (!user) { + return res.status(400).json({ status: false, msg: "User not found" }); + } + if(user.emailVerified === false|| user.cfVerified === false){ + return res.status(400).json({ status: false, msg: "Email not verified or cfID not verified" }); + } + return res.status(200).json({ status: true, msg: "User found" }); + } catch (error) { + next(error); + } +}); + diff --git a/server-side/controllers/Client/controller.js b/server-side/controllers/Client/controller.js index 676d93e..2bde0f1 100644 --- a/server-side/controllers/Client/controller.js +++ b/server-side/controllers/Client/controller.js @@ -7,6 +7,7 @@ const checkSession = require("./auth/checkSession") const userFeedback = require("./userFeedBack"); const logout = require("./auth/logout"); const ForgetPassword = require("./auth/ForgetPassword"); +const checkUser = require("./auth/checkUser"); module.exports = { login, register, @@ -16,5 +17,6 @@ module.exports = { checkSession, userFeedback, logout, - ForgetPassword + ForgetPassword, + checkUser }; \ No newline at end of file diff --git a/server-side/routes/clientRoutes.js b/server-side/routes/clientRoutes.js index 6e1bfec..abd7d50 100644 --- a/server-side/routes/clientRoutes.js +++ b/server-side/routes/clientRoutes.js @@ -4,7 +4,7 @@ const { leaderboard, contactUs, noticeboard, - register + register, } = require("../controllers/clientControllers"); const controller = require("../controllers/Client/controller"); @@ -31,19 +31,23 @@ router.post("/requestCfVerification", controller.generateCfVerificationRequestTo //For Changing Password -// @route POST api/forgetPassword +// @route POST /forgetPassword // @desc Forget Password // @access Public router.post("/forgetPassword",controller.ForgetPassword.ForgetPassword); -// @route POST api/verifyPasswordChangeOTP +// @route POST /verifyPasswordChangeOTP // @desc Confirm User // @access Public router.post("/verifyPasswordChangeOTP",verifyPasswordReq, controller.ForgetPassword.VerifyPasswordChangeOTP); -// @route POST api/confirmPasswordChange +// @route POST /confirmPasswordChange // @desc Confirm Password Change // @access Public router.post("/confirmPasswordChange",verifyPasswordReq,controller.ForgetPassword.ConfirmPasswordChange); +// @route GET /check/user/:id +// @desc Check if user exists +// @access Public +router.get("/check/user/:id",verifyCookie,controller.checkUser); module.exports = router; From 578bafd002203e9e7e55cde3a7df09fbe5870ad6 Mon Sep 17 00:00:00 2001 From: Nishant Date: Fri, 10 Jan 2025 00:41:24 +0530 Subject: [PATCH 5/8] fix: Bug fixed of Duplicate key E11000 Duplicate key error in tempUser --- .../controllers/Client/auth/register.js | 75 +++++++++++++++++-- .../controllers/Client/auth/verifyCfID.js | 2 +- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/server-side/controllers/Client/auth/register.js b/server-side/controllers/Client/auth/register.js index 4692689..f701ef4 100644 --- a/server-side/controllers/Client/auth/register.js +++ b/server-side/controllers/Client/auth/register.js @@ -36,14 +36,40 @@ const Register = AsyncErrorHandler(async (req, res, next) => { } return res.status(500).json({ success: false, message: "An error occurred while verifying codeforces ID" }); } + const hashedPassword = await bcrypt.hash(password, 10); //Check if the user already exists in database const existingUser = await User.findOne({ $or: [{ email }, { cfID }, { username }] }); - if (existingUser && !existingUser.emailVerified) { - //Delete unverified user - await User.deleteOne({ _id: existingUser._id }); - await VerificationToken.deleteOne({ email: existingUser.email }); + await User.findByIdAndUpdate(existingUser._id, { + $set: { + username, + cfID, + password:hashedPassword + } + }).save(); + await VerificationToken.deleteOne({email:email}) + //Generate new Verification tokens + const verificationCode = utils.generateVerificationCode(); + //Create a new entry in verification token model + const token = new VerificationToken({ + email: email, + code: verificationCode + }) + await token.save(); + + //Generate email + const subject = "Email Verification"; + const text = utils.createVerificationEmail({ verificationCode, subject }); + + //Send email + await SendEmail(email, subject, text); + return res.status(201).json({ + success: true, + message: "Now please verify your codeforces Id and Email to complete the Registration", + emailVerified: false + }) + } else if (existingUser && existingUser.cfVerified) { return res.status(400).json({ @@ -52,8 +78,42 @@ const Register = AsyncErrorHandler(async (req, res, next) => { }) } - //Hash the password - const hashedPassword = await bcrypt.hash(password, 10); + + //saving the data in a temporary user model which will be deleted at the time of saving actual user + const tempCheck = await tempUser.findOne({ $or: [{ email }, { cfID }, { username }] }); + if(tempCheck && !tempCheck.emailVerified) { + await tempUser.findByIdAndUpdate(tempCheck._id,{ + $set: { + username, + cfID, + password: hashedPassword + } + } + ); + await VerificationToken.deleteOne({email:email}); + //Generate new Verification tokens + const verificationCode = utils.generateVerificationCode(); + //Create a new entry in verification token model + const token = new VerificationToken({ + email: email, + code: verificationCode + }) + await token.save(); + + //Generate email + const subject = "Email Verification"; + const text = utils.createVerificationEmail({ verificationCode, subject }); + + //Send email + await SendEmail(email, subject, text); + return res.status(201).json({ + success: true, + message: "Now please verify your codeforces Id and Email to complete the Registration", + emailVerified: false + }) + + } + //Create new user let user = { username, @@ -62,7 +122,6 @@ const Register = AsyncErrorHandler(async (req, res, next) => { password: hashedPassword }; - //saving the data in a temporary user model which will be deleted at the time of saving actual user const newTempUser = new tempUser(user); await newTempUser.save(); @@ -83,7 +142,7 @@ const Register = AsyncErrorHandler(async (req, res, next) => { await SendEmail(email, subject, text); //Send response to client - res.status(201).json({ + return res.status(201).json({ success: true, message: "Now please verify your codeforces Id and Email to complete the Registration", emailVerified: false diff --git a/server-side/controllers/Client/auth/verifyCfID.js b/server-side/controllers/Client/auth/verifyCfID.js index dca1a5f..c1e6a78 100644 --- a/server-side/controllers/Client/auth/verifyCfID.js +++ b/server-side/controllers/Client/auth/verifyCfID.js @@ -93,7 +93,7 @@ const VerifyCfID = AsyncErrorHandler(async (req, res, next) => { await newUser.save(); //delete the temporary user - await tempUser.deleteOne({ cfID }); + await tempUser.findByIdAndDelete(user._id); //Success response to client res.status(200).json({ success: true, message: "cfID verified successfully" }); From d2d6973ab8895cc7722073b5bf262e862940cd66 Mon Sep 17 00:00:00 2001 From: Nishant Date: Tue, 14 Jan 2025 10:45:03 +0530 Subject: [PATCH 6/8] feat: Add models for storing the latest cfData and leaderboard CFdata contains some essential information related to present status of the CfId which will get updated as the user open its dashboard Leaderboard schema is to store the previous boards as it also allow developer to add feature to fetch the previous board and rating update can be done only as per the contest updates preprocessing total time: Codeforces Standing fetch + O(n*(time to save user data if participated) as codeforces standings is already in sorted order of its ranking also helps to get the ups and downs of the user compare to previous contest performance --- server-side/model/CFdata.js | 28 +++++++++++++++++++++++ server-side/model/leaderboard.js | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 server-side/model/CFdata.js create mode 100644 server-side/model/leaderboard.js diff --git a/server-side/model/CFdata.js b/server-side/model/CFdata.js new file mode 100644 index 0000000..56f3ee8 --- /dev/null +++ b/server-side/model/CFdata.js @@ -0,0 +1,28 @@ +const { Schema } = require("mongoose"); +const mongoose = require("mongoose"); +const CFdataSchema = new Schema({ + userId: { + type: String, + ref: "User", // Referencing the User model + unique: true, + required: true, + }, + rating: { + type: Number, + required: true, + }, + avatar: { + type: String, + required: true, + }, + rank: { + type: String, + required: true, + }, + cfusername: { + type: String, + required: true, + }, +}, { timestamps: true }); + +module.exports = mongoose.model("CFdata", CFdataSchema); diff --git a/server-side/model/leaderboard.js b/server-side/model/leaderboard.js new file mode 100644 index 0000000..5219989 --- /dev/null +++ b/server-side/model/leaderboard.js @@ -0,0 +1,39 @@ +const mongoose = require("mongoose"); +const { Schema } = mongoose; + +// Leaderboard Schema +const LeaderboardSchema = new Schema({ + contestId: { + type: String, + required: true, + unique: true, + }, + Leaderboard: { + type: [ + { + username: { + type: String, + required: true, + }, + position: { + type: Number, // Change to `String` if rank isn't numeric + }, + rating: { + type: Number, + required: true, + }, + avatar: { + type: String, // Add avatar if required in leaderboard + }, + rank: { + type: String, // Add rank if required in leaderboard + }, + }, + ], + default: [], // Ensures Leaderboard initializes as an empty array + }, +}, { timestamps: true }); // Automatically add createdAt and updatedAt timestamps + +LeaderboardSchema.index({ createdAt: -1 }); // Index to optimize recent leaderboard queries + +module.exports = mongoose.model("Leaderboard", LeaderboardSchema); From f738c3a0cf21b98f0dfcc0b3d22260b658d0f92d Mon Sep 17 00:00:00 2001 From: Nishant Date: Tue, 14 Jan 2025 10:50:00 +0530 Subject: [PATCH 7/8] feat: Add controllers for leaderboard related updates and fetch /leaderboard if a leaderboard already exist returns it if not fetch the previous board and creates new board with users in the previous board and also updatig the ranking if not participated but participated in the previous board then add them in the last with position update /updateCfdata to update the cf data as user opens the dashboard reducing the over head of multiple get req of fetching users data --- .../Client/leaderboard.controller.js | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 server-side/controllers/Client/leaderboard.controller.js diff --git a/server-side/controllers/Client/leaderboard.controller.js b/server-side/controllers/Client/leaderboard.controller.js new file mode 100644 index 0000000..2bfc544 --- /dev/null +++ b/server-side/controllers/Client/leaderboard.controller.js @@ -0,0 +1,197 @@ +const Leaderboard = require("../../model/leaderboard"); +const CFdata = require("../../model/CFdata"); // Schema to store user data +const User = require("../../model/userModel"); +const axios = require("axios"); + +const getRank = (rating) => { + switch (true) { + case rating < 1200: + return "newbie"; + case rating < 1400: + return "pupil"; + case rating < 1600: + return "specialist"; + case rating < 1800: + return "expert"; + case rating < 2000: + return "candidate"; + case rating < 2200: + return "master"; + case rating < 2400: + return "International master"; + case rating < 2800: + return "grandmaster"; + case rating < 3000: + return "International grandmaster"; + case rating < 4000: + return "Legendary grandmaster"; + case rating >= 4000: + return "Tourist"; + default: + return "Unknown"; + } +}; + +const getLeaderboard = async (req, res, next) => { + try { + const { contestId } = req.body; + + if (!contestId) { + return res.status(400).json({ status: false, msg: "Contest ID is required." }); + } + + // Check if contest already exists in the database + let contest = await Leaderboard.findOne({ contestId }); + if (contest) { + return res.status(200).json({ + status: true, + msg: "Contest already in the database", + data: contest.Leaderboard, + }); + } + + // Step 1: Fetch all CF data and users + const [cfData, users] = await Promise.all([CFdata.find(), User.find()]); + + const cfDataMap = new Map(cfData.map((user) => [user.cfusername, user])); + const userMap = new Map(users.map((user) => [user.cfID, user])); + + // Step 2: Fetch standings from Codeforces API + const standingsResponse = await axios.get( + `https://codeforces.com/api/contest.ratingChanges?contestId=${contestId}` + ); + const standings = standingsResponse.data.result; + + if (!standings.length) { + return res.status(404).json({ + status: false, + msg: "No standings data found for the contest.", + url: `https://codeforces.com/api/contest.ratingChanges?contestId=${contestId}`, + data: standings, + }); + } + + // Step 3: Create new leaderboard array + const newBoard = []; + const prevLeaderboard = await Leaderboard.findOne().sort({ createdAt: -1 }); + const prevLeaderboardMap = new Map( + prevLeaderboard?.Leaderboard.map((entry, index) => [ + entry.username, + { ...entry, index }, + ]) + ); + + await Promise.all( + standings.map(async (participant) => { + const username = participant.handle; + const previousEntry = prevLeaderboardMap.get(username); + const cfDataEntry = cfDataMap.get(username); + const userDataEntry = userMap.get(username); + + if (userDataEntry) { + const position = previousEntry + ? previousEntry.index - newBoard.length + : prevLeaderboard?.Leaderboard?.length || 0; + + newBoard.push({ + username, + position, + rating: participant.newRating, + avatar: cfDataEntry?.avatar || "https://via.placeholder.com/100", + rank: getRank(participant.newRating), + }); + + // Update CFData rating + await CFdata.findOneAndUpdate( + { userId: userDataEntry._id }, + { + rating: participant.newRating, + avatar: + cfDataEntry?.avatar || "https://via.placeholder.com/100", + rank: getRank(participant.newRating), + cfusername: userDataEntry.cfID, + }, + { upsert: true, new: true } + ); + } + }) + ); + + // Step 4: Add users from previous leaderboard who didn’t participate + prevLeaderboard?.Leaderboard.forEach((entry) => { + if (!newBoard.some((user) => user.username === entry.username)) { + newBoard.push({ + username: entry.username, + position: entry.position, + rating: entry.rating, + avatar: entry.avatar, + rank: entry.rank, + }); + } + }); + + // Step 5: Save or update the leaderboard in the database + contest = await Leaderboard.findOneAndUpdate( + { contestId }, + { contestId, Leaderboard: newBoard }, + { upsert: true, new: true } + ); + + return res.status(200).json({ + status: true, + msg: "Leaderboard updated successfully", + data: contest.Leaderboard, + }); + } catch (error) { + console.error("Error updating leaderboard:", error); + next(error); + } +}; +const updateCFData = async (req, res, next) => { + try { + const { username, avatar, rating, rank } = req.body; + + if (!username) { + return res.status(400).json({ status: false, msg: "Username is required." }); + } + + // Fetch user by cfID + const user = await User.findOne({ cfID: username }); + if (!user) { + return res.status(404).json({ status: false, msg: "User not found." }); + } + + // Check if a CFdata entry exists for the user + let cfData = await CFdata.findOne({ userId: user._id }); + + if (!cfData) { + // Create a new CFdata entry + cfData = new CFdata({ + userId: user._id, + rating, + avatar, + rank, + cfusername: username, + }); + await cfData.save(); + } else { + // Update CFdata entry if there are changes + if (cfData.rating !== rating || cfData.avatar !== avatar || cfData.rank !== rank) { + cfData.rating = rating; + cfData.avatar = avatar; + cfData.rank = rank; + await cfData.save(); + } + } + return res.status(200).json({ status: true, msg: "CF data updated successfully.", data: cfData }); + } catch (error) { + console.error("Error fetching CF data:", error); + next(error); + } +}; + + +module.exports = { + getLeaderboard, + updateCFData, +}; From 69d2df7327e6762cfdc1677a3383b9155c9d249f Mon Sep 17 00:00:00 2001 From: Nishant Date: Tue, 14 Jan 2025 10:53:27 +0530 Subject: [PATCH 8/8] Chore: refactor the leaderboard.jsx frontend file and endpoints added --- client-side/src/pages/DashBoard/dashboard.jsx | 2 +- .../src/pages/Leaderboard/LeaderUser.jsx | 37 +++--- .../src/pages/Leaderboard/Leaderboard.css | 20 ++++ .../src/pages/Leaderboard/Leaderboard.jsx | 108 +++++++++--------- server-side/controllers/clientControllers.js | 30 ++--- server-side/routes/clientRoutes.js | 15 ++- 6 files changed, 120 insertions(+), 92 deletions(-) diff --git a/client-side/src/pages/DashBoard/dashboard.jsx b/client-side/src/pages/DashBoard/dashboard.jsx index e68b8bb..7c12b27 100644 --- a/client-side/src/pages/DashBoard/dashboard.jsx +++ b/client-side/src/pages/DashBoard/dashboard.jsx @@ -118,7 +118,7 @@ export default function UserHome() { userData.status = userDataAPI.data.status; userData.data = userDataAPI.data.result[0]; - + const userUpdate = await axios.post(`${process.env.REACT_APP_SERVER_BASE_URL}/updateCFData`,{username:userData.data.handle,avatar:userData.data.avatar,rating:userData.data.rating,rank:userData.data.rank},{withCredentials: true}); userRating.status = userRatingAPI.data.status; userRating.data = userRatingAPI.data.result; diff --git a/client-side/src/pages/Leaderboard/LeaderUser.jsx b/client-side/src/pages/Leaderboard/LeaderUser.jsx index 7652f14..6fb3f4c 100644 --- a/client-side/src/pages/Leaderboard/LeaderUser.jsx +++ b/client-side/src/pages/Leaderboard/LeaderUser.jsx @@ -19,12 +19,6 @@ function LeaderUser(props) { return (
- {`${name} - #{rank} - {name} - - -
+ {`${name} + #{rank} + {name} + + +
+
@@ -63,18 +64,18 @@ function LeaderUser(props) { } function getRankColor(rank) { - switch(rank) { - case 'newbie': return 'gray'; - case 'pupil': return 'green'; - case 'specialist': return 'cyan'; - case 'expert': return 'blue'; - case 'candidate master': return 'purple'; - case 'master': return 'orange'; - case 'international master': return 'red'; - case 'grandmaster': return 'darkred'; - case 'international grandmaster': return 'darkred'; - case 'legendary grandmaster': return 'darkred'; - default: return 'black'; + switch(rank.toLowerCase()) { + case 'newbie': return '#A0A0A0'; // gray + case 'pupil': return '#66CDAA'; // medium sea green + case 'specialist': return '#40E0D0'; // turquoise (cyan) + case 'expert': return '#4682B4'; // steel blue (blue) + case 'candidate master': return '#8A2BE2'; // blue violet (purple) + case 'master': return '#FF7F50'; // coral (orange) + case 'international master': return '#B22222'; // firebrick (red) + case 'grandmaster': return '#8B0000'; // dark red + case 'international grandmaster': return '#8B0000'; // dark red + case 'legendary grandmaster': return '#8B0000'; // dark red + default: return '#000000'; // black } } diff --git a/client-side/src/pages/Leaderboard/Leaderboard.css b/client-side/src/pages/Leaderboard/Leaderboard.css index 0e5f4e0..9f4b50e 100644 --- a/client-side/src/pages/Leaderboard/Leaderboard.css +++ b/client-side/src/pages/Leaderboard/Leaderboard.css @@ -1,3 +1,23 @@ +.leader-heading { + font-size: 36px; + font-weight: bold; + color: #3f3f3f; + margin-bottom: 10px; +} + +.leader-description { + font-size: 16px; + color: #555; + line-height: 1.6; + max-width: 80%; + margin: 0 auto; +} +.leader-heading-container { + text-align: center; + margin-bottom: 20px; + margin-top:20px; +} + .leader-box { display: flex; justify-content: space-between; diff --git a/client-side/src/pages/Leaderboard/Leaderboard.jsx b/client-side/src/pages/Leaderboard/Leaderboard.jsx index ced1c28..0e215ce 100644 --- a/client-side/src/pages/Leaderboard/Leaderboard.jsx +++ b/client-side/src/pages/Leaderboard/Leaderboard.jsx @@ -16,87 +16,85 @@ export default function Leaderboard() { ); - // Function to sort users by their ratings (new or usual) - const sortUsersByRating = (userBoardInfo, userRatingsKey) => { - return [...userBoardInfo].sort((a, b) => { - const aRating = a[userRatingsKey] || 0; - const bRating = b[userRatingsKey] || 0; - return bRating - aRating; // Sort in descending order - }); - }; const updatePageHtml = async () => { try { - const leaderboardAPIResponse = await axios.post( - `${process.env.REACT_APP_SERVER_BASE_URL}/leaderboard`, - { cfID: user.cfID }, - { withCredentials: true } - ); - - const userBoardInfo = leaderboardAPIResponse.data.data; // Get the latest contest ID const contests = await axios.get('https://codeforces.com/api/contest.list'); const latestContest = contests.data.result.find((contest) => contest.phase === 'FINISHED'); const latestContestId = latestContest.id; - - // Get ratings from the latest contest - const ratingChanges = await axios.get(`https://codeforces.com/api/contest.ratingChanges?contestId=${latestContestId}`); - const participants = ratingChanges.data.result; - console.log(participants); - // Fetch user info for usual ratings - const userRatings = await Promise.all( - userBoardInfo.map(async (user) => { - const userInfo = await axios.get(`https://codeforces.com/api/user.info?handles=${user.cfID}`); - const userDetails = userInfo.data.result[0]; - console.log(userDetails); - return { - cfID: user.cfID, - avatar:userDetails.avatar, - rank:userDetails.rank, - usualRating: userDetails.rating || 0, - newRating: participants.find((p) => p.handle === user.cfID)?.newRating || 0, - }; - }) + const leaderboardAPIResponse = await axios.post( + `${process.env.REACT_APP_SERVER_BASE_URL}/leaderboard`, + { contestId: latestContestId } ); - // Sort users by new ratings and usual ratings - userRatings.sort((a, b) => b.usualRating - a.usualRating); - const sortedByNewRating = sortUsersByRating(userRatings, 'newRating'); - const sortedByUsualRating = sortUsersByRating(userRatings, 'usualRating'); + const userBoardInfo = leaderboardAPIResponse.data.data; + + // // Get ratings from the latest contest + // const ratingChanges = await axios.get(`https://codeforces.com/api/contest.ratingChanges?contestId=${latestContestId}`); + // const participants = ratingChanges.data.result; + // console.log(participants); + // // Fetch user info for usual ratings + // const userRatings = await Promise.all( + // userBoardInfo.map(async (user) => { + // const userInfo = await axios.get(`https://codeforces.com/api/user.info?handles=${user.cfID}`); + // const userDetails = userInfo.data.result[0]; + // console.log(userDetails); + // return { + // cfID: user.cfID, + // avatar:userDetails.avatar, + // rank:userDetails.rank, + // usualRating: userDetails.rating || 0, + // newRating: participants.find((p) => p.handle === user.cfID)?.newRating || 0, + // }; + // }) + // ); + + // // Sort users by new ratings and usual ratings + // userRatings.sort((a, b) => b.usualRating - a.usualRating); + // const sortedByNewRating = sortUsersByRating(userRatings, 'newRating'); + // const sortedByUsualRating = sortUsersByRating(userRatings, 'usualRating'); - // Calculate position changes - const leaderComponents = sortedByNewRating.map((user, index) => { - const oldPosition = sortedByUsualRating.findIndex((u) => u.cfID === user.cfID) + 1; - const newPosition = index + 1; - const positionChange = oldPosition-newPosition; - console.log(user); + // // Calculate position changes + // const leaderComponents = sortedByNewRating.map((user, index) => { + // const oldPosition = sortedByUsualRating.findIndex((u) => u.cfID === user.cfID) + 1; + // const newPosition = index + 1; + // const positionChange = oldPosition-newPosition; + // console.log(user); + const leaderComponents = userBoardInfo.map((user,index) => { return ( ); }); setPageHtml(<> -
+
-
Leaderboard
-

- Rankings are based on the latest contest results. Ratings of inactive participants are considered 0. -

-
{leaderComponents}
+
+

Leaderboard

+

+ Rankings are based on the latest contest results. Ratings of inactive participants are considered 0. +

+
+ +
+ {leaderComponents} +
+ ); } catch (err) { setPageHtml( diff --git a/server-side/controllers/clientControllers.js b/server-side/controllers/clientControllers.js index a625ac9..c2dd92e 100644 --- a/server-side/controllers/clientControllers.js +++ b/server-side/controllers/clientControllers.js @@ -47,23 +47,23 @@ module.exports.videos = async (req, res, next) => { next(ex); } }; -module.exports.leaderboard = async (req, res, next) => { - try { - const {decoded}= req; - const {cookieID}= decoded; - const session = await ClientSessions.findOne({cookieID}); - if (!session || cookieID !== session.cookieID) { - return res.status(401).json({ status: false, msg: "Session expired or invalid" }); - } +// module.exports.leaderboard = async (req, res, next) => { +// try { +// const {decoded}= req; +// const {cookieID}= decoded; +// const session = await ClientSessions.findOne({cookieID}); +// if (!session || cookieID !== session.cookieID) { +// return res.status(401).json({ status: false, msg: "Session expired or invalid" }); +// } - const cfID = await Users.find({ emailVerified: true, cfVerified: true }).select(["cfID"]); - return res.json({ status: true, data: cfID }); - } - catch (error) { - next(error); - } +// const cfID = await Users.find({ emailVerified: true, cfVerified: true }).select(["cfID"]); +// return res.json({ status: true, data: cfID }); +// } +// catch (error) { +// next(error); +// } -}; +// }; module.exports.register = async (req, res, next) => { try { diff --git a/server-side/routes/clientRoutes.js b/server-side/routes/clientRoutes.js index abd7d50..71eb7a4 100644 --- a/server-side/routes/clientRoutes.js +++ b/server-side/routes/clientRoutes.js @@ -1,12 +1,12 @@ const { educationCategories, videos, - leaderboard, + // leaderboard, contactUs, noticeboard, register, } = require("../controllers/clientControllers"); - +const leaderboard = require("../controllers/Client/leaderboard.controller"); const controller = require("../controllers/Client/controller"); const verifyCookie = require("../middleware/verifyCookie"); const verifyPasswordReq = require("../middleware/verifyPasswordReq"); @@ -15,7 +15,15 @@ const router = require("express").Router(); // Routes that require authentication router.post("/education", verifyCookie, educationCategories); router.post("/education/videos", verifyCookie, videos); -router.post("/leaderboard", verifyCookie, leaderboard); +//@route POST /leaderboard +//@desc Get leaderboard +//@access Private +router.post("/leaderboard",leaderboard.getLeaderboard ); +//@route POST /updateCFData +//@desc Update CF Data +//@access Private +router.post("/updateCFData", leaderboard.updateCFData); + router.post("/feedback", verifyCookie, controller.userFeedback); router.post("/logout", verifyCookie, controller.logout); router.get("/check/session", verifyCookie, controller.checkSession); @@ -50,4 +58,5 @@ router.post("/confirmPasswordChange",verifyPasswordReq,controller.ForgetPassword // @desc Check if user exists // @access Public router.get("/check/user/:id",verifyCookie,controller.checkUser); + module.exports = router;