From 9afe87e7e9abb6d44abf247dbd4bc76270965345 Mon Sep 17 00:00:00 2001 From: aasrithasure Date: Fri, 7 Aug 2026 18:54:52 +0530 Subject: [PATCH] feat: add interview streak tracking feature --- backend/constants/achievements.js | 3 ++ backend/controllers/achievementController.js | 16 ++++++- backend/controllers/authController.js | 14 ++++++ backend/controllers/sessionController.js | 47 ++++++++++++++++++++ backend/models/User.js | 5 +++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/backend/constants/achievements.js b/backend/constants/achievements.js index 48276e5f..eabb8e6b 100644 --- a/backend/constants/achievements.js +++ b/backend/constants/achievements.js @@ -10,6 +10,9 @@ const VALID_ACHIEVEMENTS = new Set([ "Resume Expert", "DSA Beginner", "DSA Master", + "3-Day Streak", + "7-Day Streak", + "30-Day Streak", ]); module.exports = { VALID_ACHIEVEMENTS }; \ No newline at end of file diff --git a/backend/controllers/achievementController.js b/backend/controllers/achievementController.js index 14988928..8cf01091 100644 --- a/backend/controllers/achievementController.js +++ b/backend/controllers/achievementController.js @@ -3,8 +3,22 @@ const { VALID_ACHIEVEMENTS } = require('../constants/achievements'); exports.getAchievements = async (req, res) => { try { - const user = await User.findById(req.user._id).select('unlockedAchievements'); + const user = await User.findById(req.user._id).select('unlockedAchievements lastPracticeDate currentStreak'); if (!user) return res.status(404).json({ success: false, error: 'User not found' }); + + // Reset streak to 0 if one or more calendar days were missed + if (user.lastPracticeDate && user.currentStreak > 0) { + const now = new Date(); + const d1 = new Date(user.lastPracticeDate); + const utc1 = Date.UTC(d1.getUTCFullYear(), d1.getUTCMonth(), d1.getUTCDate()); + const utc2 = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); + const diffDays = Math.floor((utc2 - utc1) / (1000 * 60 * 60 * 24)); + if (diffDays > 1) { + user.currentStreak = 0; + await user.save(); + } + } + res.json({ success: true, unlockedAchievements: user.unlockedAchievements }); } catch (err) { res.status(500).json({ success: false, error: "A server error occurred" }); diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 46817a8f..0c53d155 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -401,6 +401,20 @@ const getUserProfile = async (req, res) => { if (!user) { return res.status(404).json({ success: false, message: "Requested user profile not found" }); } + + // Reset streak to 0 if one or more calendar days were missed + if (user.lastPracticeDate && user.currentStreak > 0) { + const now = new Date(); + const d1 = new Date(user.lastPracticeDate); + const utc1 = Date.UTC(d1.getUTCFullYear(), d1.getUTCMonth(), d1.getUTCDate()); + const utc2 = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); + const diffDays = Math.floor((utc2 - utc1) / (1000 * 60 * 60 * 24)); + if (diffDays > 1) { + user.currentStreak = 0; + await user.save(); + } + } + res.json(user); } catch (error) { console.error("Get profile error:", error); diff --git a/backend/controllers/sessionController.js b/backend/controllers/sessionController.js index 4da73ca6..f4c449e0 100644 --- a/backend/controllers/sessionController.js +++ b/backend/controllers/sessionController.js @@ -1,6 +1,7 @@ const Session = require("../models/Session"); const Question = require("../models/Question"); const mongoose = require("mongoose"); +const User = require("../models/User"); const MAX_SESSIONS = Number(process.env.MAX_SESSIONS) || 50; @@ -92,6 +93,52 @@ await createdSession[0].save({ session: mongoSession, }); + // Update user streak on successful session creation + const user = await User.findById(userId).session(mongoSession); + if (user) { + const now = new Date(); + const getUTCDayDifference = (date1, date2) => { + const d1 = new Date(date1); + const d2 = new Date(date2); + const utc1 = Date.UTC(d1.getUTCFullYear(), d1.getUTCMonth(), d1.getUTCDate()); + const utc2 = Date.UTC(d2.getUTCFullYear(), d2.getUTCMonth(), d2.getUTCDate()); + const msPerDay = 1000 * 60 * 60 * 24; + return Math.floor((utc2 - utc1) / msPerDay); + }; + + if (!user.lastPracticeDate) { + user.currentStreak = 1; + } else { + const diff = getUTCDayDifference(user.lastPracticeDate, now); + if (diff === 1) { + user.currentStreak += 1; + } else if (diff > 1) { + user.currentStreak = 1; + } // if diff === 0, keep current streak (already practiced today) + } + + user.lastPracticeDate = now; + + if (user.currentStreak > (user.longestStreak || 0)) { + user.longestStreak = user.currentStreak; + } + + // Check milestone badges + const streakMilestones = [ + { days: 3, badge: "3-Day Streak" }, + { days: 7, badge: "7-Day Streak" }, + { days: 30, badge: "30-Day Streak" } + ]; + + for (const milestone of streakMilestones) { + if (user.currentStreak >= milestone.days && !user.unlockedAchievements.includes(milestone.badge)) { + user.unlockedAchievements.push(milestone.badge); + } + } + + await user.save({ session: mongoSession }); + } + res.status(201).json({ success: true, session: createdSession[0], diff --git a/backend/models/User.js b/backend/models/User.js index 5d0d6b3c..cab7b717 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -54,6 +54,11 @@ const UserSchema = new mongoose.Schema( unlockedAchievements: { type: [String], default: [] }, + // Streak Tracking + currentStreak: { type: Number, default: 0 }, + longestStreak: { type: Number, default: 0 }, + lastPracticeDate: { type: Date, default: null }, + //Email Verification isEmailVerified: { type: Boolean, default: false }, emailVerificationToken: { type: String, default: null },