Skip to content

Commit cd6eb9e

Browse files
authored
Merge pull request #127 from nirakarpatel/main
Feature Overhaul & Critical Bug Fixes: XP Sync, Leaderboard & Profile UI
2 parents 556d3cb + 62095c2 commit cd6eb9e

19 files changed

Lines changed: 213 additions & 156 deletions

admin-client/src/pages/Dashboard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ const Dashboard = () => {
170170
{/* ── Greeting ────────────────────────── */}
171171
<motion.div {...fd(0.04)}>
172172
<h1 className="text-3xl font-black text-primary mb-1">
173-
{greeting.heading.replace("{username}", user?.username || "Operative")}
173+
{greeting.heading.replace("{username}", user?.name || user?.username || "Operative")}
174174
</h1>
175175
<p className="text-secondary text-sm">
176176
{greeting.subtext}

admin-client/src/pages/admin/QuestionSetsTab.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ const QuestionSetsTab = () => {
262262
<div><label className="field-label">Week Number</label>
263263
<div className="relative"><FiCalendar className="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary"/><input required type="number" min="1" className="field-input pl-9" value={form.weekNumber} onChange={e=>setForm({...form,weekNumber:Number(e.target.value)})}/></div>
264264
</div>
265-
<div><label className="field-label">Deadline</label><input required type="date" className="field-input" value={form.deadline} onChange={e=>setForm({...form,deadline:e.target.value})}/></div>
265+
<div><label className="field-label">Deadline</label><input required type="date" min={new Date().toISOString().split('T')[0]} className="field-input" value={form.deadline} onChange={e=>setForm({...form,deadline:e.target.value})}/></div>
266266
</div>
267267
</BaseCard>
268268

admin-client/src/pages/chief/admin/QuestionSetsTab.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ const QuestionSetsTab = () => {
158158
</div>
159159
<div>
160160
<label className="field-label">Deadline</label>
161-
<input required type="date" className="field-input" value={form.deadline} onChange={e => setForm({...form, deadline: e.target.value})} />
161+
<input required type="date" min={new Date().toISOString().split('T')[0]} className="field-input" value={form.deadline} onChange={e => setForm({...form, deadline: e.target.value})} />
162162
</div>
163163
</div>
164164
</BaseCard>

client/src/components/Footer.jsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { Link } from 'react-router-dom';
33
import { FiInstagram, FiLinkedin } from 'react-icons/fi';
4-
import { FaWhatsapp, FaDiscord } from 'react-icons/fa';
4+
import { FaWhatsapp } from 'react-icons/fa';
55
import Logo from './Logo';
66

77
const Footer = () => {
@@ -82,16 +82,7 @@ const Footer = () => {
8282
>
8383
<FiLinkedin size={16} />
8484
</a>
85-
<a
86-
href="https://discord.gg/csMG6yKzc"
87-
target="_blank"
88-
rel="noopener noreferrer"
89-
title="Discord"
90-
className="w-10 h-10 rounded-full border flex items-center justify-center text-secondary hover:text-accent hover:border-accent hover:bg-accent/5 transition-all duration-300"
91-
style={{ borderColor: `rgba(var(--accent-rgb), 0.25)` }}
92-
>
93-
<FaDiscord size={16} />
94-
</a>
85+
9586
</div>
9687
</div>
9788
</div>

client/src/components/ProfileSidebar.jsx

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,58 @@ const FALLBACK_BADGES = [
3232
/* ── XP Level helper ─────────────────────────────────────── */
3333
const XP_PER_LEVEL = 500;
3434
const getLevel = (xp) => Math.floor(xp / XP_PER_LEVEL) + 1;
35-
const getLevelPct = (xp) => ((xp % XP_PER_LEVEL) / XP_PER_LEVEL) * 100;
3635

3736
/* ── Animated XP bar ─────────────────────────────────────── */
38-
const XPBar = ({ xp }) => {
37+
const XPBar = ({ xp, loginXp = 0, challengeXp = 0, loginCount = 0 }) => {
3938
const ref = useRef(null);
4039
const inView = useInView(ref, { once: true });
41-
const pct = getLevelPct(xp);
40+
const currentLevelXp = xp % XP_PER_LEVEL;
41+
const pct = (currentLevelXp / XP_PER_LEVEL) * 100;
4242
const lvl = getLevel(xp);
43+
44+
const loginRatio = xp > 0 ? loginXp / xp : 0;
45+
4346
return (
44-
<div ref={ref} className="space-y-1.5">
47+
<div ref={ref} className="space-y-1.5 relative group">
4548
<div className="flex items-center justify-between text-[10px] font-black uppercase tracking-widest">
4649
<span className="text-tertiary">Level {lvl}</span>
47-
<span style={{ color: "rgb(var(--accent-rgb))" }}>{xp} XP</span>
50+
<span style={{ color: "rgb(var(--accent-rgb))" }}>{currentLevelXp} / {XP_PER_LEVEL} XP</span>
4851
</div>
49-
<div className="h-1.5 rounded-full bg-black/[0.06] dark:bg-white/[0.06] overflow-hidden">
52+
53+
{/* XP Breakdown Tooltip */}
54+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-max bg-black/90 dark:bg-white/90 text-white dark:text-black text-[10px] font-bold p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 shadow-xl shadow-black/20 text-center flex flex-col gap-1 pointer-events-none">
55+
<div className="flex justify-between gap-4">
56+
<span className="text-[#3b82f6]">Daily Login XP:</span>
57+
<span>{loginXp} XP ({loginCount} days)</span>
58+
</div>
59+
<div className="flex justify-between gap-4">
60+
<span className="text-[#ec4899]">Challenges XP:</span>
61+
<span>{challengeXp} XP</span>
62+
</div>
63+
<div className="mt-1 pt-1 border-t border-white/20 dark:border-black/20 flex justify-between gap-4">
64+
<span className="text-white/60 dark:text-black/60">Total XP:</span>
65+
<span>{xp} XP</span>
66+
</div>
67+
{/* Little triangle pointing down */}
68+
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-black/90 dark:border-t-white/90" />
69+
</div>
70+
71+
<div className="h-1.5 rounded-full bg-black/[0.06] dark:bg-white/[0.06] overflow-hidden flex">
5072
<motion.div
5173
className="h-full rounded-full"
5274
style={{
53-
background: "linear-gradient(90deg, rgba(var(--accent-rgb),0.9), rgba(168,85,247,0.9))",
54-
boxShadow: "0 0 10px rgba(var(--accent-rgb),0.5)",
75+
background: `linear-gradient(90deg, #3b82f6 0%, #a855f7 ${loginRatio * 100}%, #ec4899 100%)`,
76+
boxShadow: `0 0 10px rgba(168,85,247,0.5)`
5577
}}
5678
initial={{ width: 0 }}
5779
animate={{ width: inView ? `${pct}%` : 0 }}
5880
transition={{ duration: 1.4, ease: "easeOut" }}
5981
/>
6082
</div>
61-
<p className="text-[10px] text-tertiary">
62-
{XP_PER_LEVEL - (xp % XP_PER_LEVEL)} XP to Level {lvl + 1}
63-
</p>
83+
<div className="flex justify-between text-[10px] text-tertiary">
84+
<span>{XP_PER_LEVEL - currentLevelXp} XP to Level {lvl + 1}</span>
85+
<span className="font-semibold text-secondary">Total: {xp} XP</span>
86+
</div>
6487
</div>
6588
);
6689
};
@@ -115,6 +138,9 @@ const ProfileSidebar = ({ user, summary, profile, badges }) => {
115138
const streak = profile?.streak ?? 0;
116139
const maxStreak = profile?.maxStreak ?? 0;
117140
const xp = profile?.stats?.totalPoints ?? profile?.totalPoints ?? 0;
141+
const loginXp = profile?.stats?.loginXp ?? profile?.loginXp ?? 0;
142+
const challengeXp = profile?.stats?.challengeXp ?? profile?.challengeXp ?? 0;
143+
const loginCount = profile?.stats?.loginCount ?? profile?.loginCount ?? 0;
118144
const rank = profile?.rank ?? "—";
119145
const roleName = user?.customTitle || (user?.role === "admin" ? "Admin"
120146
: user?.role === "clan-chief" ? "Clan Chief"
@@ -295,7 +321,7 @@ const ProfileSidebar = ({ user, summary, profile, badges }) => {
295321
</div>
296322

297323
{/* XP Level bar */}
298-
<XPBar xp={xp} />
324+
<XPBar xp={xp} loginXp={loginXp} challengeXp={challengeXp} loginCount={loginCount} />
299325

300326
{/* Divider */}
301327
<div className="h-px bg-black/[0.08] dark:bg-white/[0.08]" />

client/src/pages/ChallengeDetails.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,11 @@ const ChallengeDetails = () => {
568568
<span className="hidden sm:inline">Missions</span>
569569
</Link>
570570
<div className="w-px h-4 bg-black/10 dark:bg-white/10" />
571-
<a href={challenge.link} target="_blank" rel="noopener noreferrer">
571+
<a
572+
href={challenge.link || `https://leetcode.com/problems/${challenge.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}/`}
573+
target="_blank"
574+
rel="noopener noreferrer"
575+
>
572576
<h1 className="text-sm sm:text-base font-bold truncate flex flex-row items-center gap-1.5 hover:text-accent transition-colors">
573577
{challenge.title} <FiExternalLink size={12} />
574578
</h1>

client/src/pages/Dashboard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ const Dashboard = () => {
392392
{/* ── Greeting ────────────────────────── */}
393393
<motion.div {...fd(0.04)}>
394394
<h2 className="text-2xl font-black text-primary mb-1 font-h2">
395-
{greeting.heading.replace("{username}", user?.username || "Operative")}
395+
{greeting.heading.replace("{username}", user?.name || user?.username || "Operative")}
396396
</h2>
397397
<p className="text-secondary text-sm">
398398
{greeting.subtext}

client/src/pages/Leaderboard.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,17 @@ const Leaderboard = () => {
217217
);
218218
}, [rows, search]);
219219

220-
const topThree = visibleRows.slice(0, 3);
220+
const topThree = useMemo(() => {
221+
if (search.trim()) {
222+
return visibleRows.slice(0, 3);
223+
}
224+
225+
if (leaderType === "individual") {
226+
return leaderboardQuery.data?.meta?.topThree || visibleRows.slice(0, 3);
227+
}
228+
229+
return visibleRows.slice(0, 3);
230+
}, [leaderType, search, visibleRows, leaderboardQuery.data?.meta?.topThree]);
221231
const myRow =
222232
leaderType === "individual"
223233
? rows.find((row) => row.username === user?.username)

server/fix_badges.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

server/src/features/auth/auth.controller.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,21 @@ const googleLogin = async (req, res, next) => {
403403
const lastLogin = user.lastLoginDate ? new Date(user.lastLoginDate) : null;
404404
const isFirstLoginToday = !lastLogin || lastLogin < today;
405405

406-
if (isFirstLoginToday && user.usernameSet) {
406+
const XpLog = require('../users/XpLog.model');
407+
const existingDailyLog = await XpLog.exists({
408+
userId: user._id,
409+
reason: 'Daily Login',
410+
createdAt: { $gte: today }
411+
});
412+
413+
if (isFirstLoginToday && !existingDailyLog && user.usernameSet) {
407414
user.points = (user.points || 0) + 50;
408415
dailyXpAwarded = true;
416+
await XpLog.create({
417+
userId: user._id,
418+
amount: 50,
419+
reason: 'Daily Login',
420+
});
409421
}
410422

411423
user.lastLoginDate = now;
@@ -548,11 +560,24 @@ const getMe = async (req, res, next) => {
548560
const now = new Date();
549561
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
550562
const lastLogin = user.lastLoginDate ? new Date(user.lastLoginDate) : null;
563+
const isFirstLoginToday = !lastLogin || lastLogin < today;
551564

552-
if (!lastLogin || lastLogin < today) {
565+
const XpLog = require('../users/XpLog.model');
566+
const existingDailyLog = await XpLog.exists({
567+
userId: user._id,
568+
reason: 'Daily Login',
569+
createdAt: { $gte: today }
570+
});
571+
572+
if (isFirstLoginToday && !existingDailyLog) {
553573
if (user.usernameSet) {
554574
user.points = (user.points || 0) + 50;
555575
dailyXpAwarded = true;
576+
await XpLog.create({
577+
userId: user._id,
578+
amount: 50,
579+
reason: 'Daily Login',
580+
});
556581
}
557582
user.lastLoginDate = now;
558583
await User.findByIdAndUpdate(user._id, {

0 commit comments

Comments
 (0)