Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/src/sections/DailyChallenges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getAllProblems } from '@/api/content';
import { updateProblemStatus, getUserProgress } from '@/api/userActions';
import { useAuth } from '@/contexts/AuthContext';
import { toast } from 'sonner';
import { SOLVE_XP } from '@/utils/xpConfig';

interface DailyChallengesProps {
onBack: () => void;
Expand Down Expand Up @@ -140,7 +141,7 @@ export function DailyChallenges({ onBack }: DailyChallengesProps) {

try {
await updateProblemStatus(problemMongoId, wasCompleted ? 'TODO' : 'SOLVED');
if (!wasCompleted) toast.success('Challenge problem solved! +25 XP');
if (!wasCompleted) toast.success(`Challenge problem solved! +${SOLVE_XP} XP`);
refreshProfile();
} catch {
setCompletedProblems(prev => {
Expand Down Expand Up @@ -205,7 +206,7 @@ export function DailyChallenges({ onBack }: DailyChallengesProps) {
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full glass">
<Zap className="w-5 h-5 text-[#ffd700]" />
<span className="text-white font-medium">{challengesSolved * 25} XP Earned</span>
<span className="text-white font-medium">{challengesSolved * SOLVE_XP} XP Earned</span>
</div>
</div>
</motion.div>
Expand Down Expand Up @@ -309,7 +310,7 @@ export function DailyChallenges({ onBack }: DailyChallengesProps) {
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-white/5">
<Zap className="w-4 h-4 text-[#ffd700]" />
<span className="text-sm text-white/40">
{isCompleted ? 'Earned' : 'Reward'}: <span className="text-[#ffd700]">+25 XP</span>
{isCompleted ? 'Earned' : 'Reward'}: <span className="text-[#ffd700]">+{SOLVE_XP} XP</span>
</span>
</div>
</motion.div>
Expand Down
19 changes: 16 additions & 3 deletions app/src/sections/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { useAuth } from '@/contexts/AuthContext';
import { getAllProblems, getAllTopics } from '@/api/content';
import { getUserProgress, getDashboardStats } from '@/api/userActions';
import { SOLVE_XP, XP_PER_LEVEL, calculateLevel } from '@/utils/xpConfig';

interface DashboardProps {
onNavigate: (view: 'home' | 'dashboard' | 'topic' | 'problems' | 'notes' | 'leaderboard' | 'daily-challenges', topicId?: string) => void;
Expand Down Expand Up @@ -140,7 +141,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {

const totalSolved = solvedIds.size;
const totalProblems = problems.length;
const xpPoints = profile?.xp_points ?? totalSolved * 25;
const xpPoints = profile?.xp_points ?? totalSolved * SOLVE_XP;

let easy = 0, medium = 0, hard = 0;
let easyTotal = 0, mediumTotal = 0, hardTotal = 0;
Expand Down Expand Up @@ -281,7 +282,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
}

const level = Math.floor((stats.xpPoints) / 1000) + 1;
const level = calculateLevel(stats.xpPoints);

if (loading) {
return (
Expand Down Expand Up @@ -407,6 +408,8 @@ export function Dashboard({ onNavigate }: DashboardProps) {
>
{[
{ label: 'Problems Solved', value: animSolved, sub: `of ${stats.totalProblems}`, icon: CheckCircle2, color: '#a088ff', glow: 'rgba(160,136,255,0.15)' },
{ label: 'XP Points', value: animXP, sub: `Level ${level}`, icon: Zap, color: '#ffd700', glow: 'rgba(255,215,0,0.12)', tooltip: `Earn ${SOLVE_XP} XP per solved problem. Every ${XP_PER_LEVEL.toLocaleString()} XP = 1 Level.` },
{ label: 'Day Streak', value: animStreak, sub: stats.currentStreak > 0 ? 'Keep it up!' : 'Solve to start!', icon: Flame, color: '#ff8a63', glow: 'rgba(255,138,99,0.12)' },
{ label: 'XP Points', value: animXP, sub: `Level ${level}`, icon: Zap, color: '#ffd700', glow: 'rgba(255,215,0,0.12)' },
{ label: 'Day Streak', value: animStreak, sub: 'Resets at midnight UTC', icon: Flame, color: '#ff8a63', glow: 'rgba(255,138,99,0.12)' },
{ label: 'Global Rank', value: `#${rankInfo.rank}`, sub: `Top ${rankInfo.topPercent}%`, icon: Trophy, color: '#88ff9f', glow: 'rgba(136,255,159,0.12)' },
Expand All @@ -416,7 +419,17 @@ export function Dashboard({ onNavigate }: DashboardProps) {
whileHover={{ scale: 1.03, y: -4 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className="relative glass rounded-2xl p-5 overflow-hidden group cursor-default"
role="status"
aria-label={`${stat.label}: ${stat.value}`}
>
{stat.tooltip && (
<div className="absolute top-2 right-2 z-20">
<div className="relative group/tooltip">
<div className="w-4 h-4 rounded-full bg-white/10 flex items-center justify-center text-[10px] text-white/40 cursor-help">?</div>
<div className="absolute bottom-full right-0 mb-2 w-48 p-2 rounded-lg bg-[#1e1e2d] border border-white/10 text-xs text-white/70 opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-30">
{stat.tooltip}
</div>
</div>
{isRefreshing && (
<div className="absolute inset-0 bg-white/5 backdrop-blur-sm flex items-center justify-center z-10 rounded-2xl">
<RefreshCw className="w-5 h-5 text-white/50 animate-spin" />
Expand Down Expand Up @@ -831,7 +844,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
</div>
</div>
</div>
<span className="text-xs font-medium text-[#88ff9f]/70">+25 XP</span>
<span className="text-xs font-medium text-[#88ff9f]/70">+{SOLVE_XP} XP</span>
</motion.div>
);
}) : (
Expand Down
3 changes: 2 additions & 1 deletion app/src/sections/Problems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { updateProblemStatus, toggleBookmark as apiToggleBookmark, getUserProgre
import { useAuth } from '@/contexts/AuthContext';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { SOLVE_XP } from '@/utils/xpConfig';

export function Problems() {
const { data: problemsData = [], isLoading: problemsLoading } = useQuery({
Expand Down Expand Up @@ -125,7 +126,7 @@ export function Problems() {

try {
await updateProblemStatus(problemMongoId, wasCompleted ? 'TODO' : 'SOLVED');
if (!wasCompleted) toast.success('Problem marked as complete! +25 XP');
if (!wasCompleted) toast.success(`Problem marked as complete! +${SOLVE_XP} XP`);
// Refresh profile so nav XP updates immediately
refreshProfile();
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion app/src/sections/TopicDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { updateProblemStatus, toggleBookmark as apiToggleBookmark, getUserProgre
import { useAuth } from '@/contexts/AuthContext';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { SOLVE_XP } from '@/utils/xpConfig';

interface TopicDetailProps {
topicId: string;
Expand Down Expand Up @@ -126,7 +127,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {

try {
await updateProblemStatus(problemMongoId, wasCompleted ? 'TODO' : 'SOLVED');
if (!wasCompleted) toast.success('Problem marked as complete! +25 XP');
if (!wasCompleted) toast.success(`Problem marked as complete! +${SOLVE_XP} XP`);
// Refresh profile so nav XP updates immediately
refreshProfile();
} catch (e) {
Expand Down
20 changes: 20 additions & 0 deletions app/src/utils/xpConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* XP and Level Configuration
*
* Central source of truth for all XP values and level calculations.
* Change these values once — they propagate everywhere.
*/

/** XP awarded for solving a problem */
export const SOLVE_XP = 25;

/** XP required per level */
export const XP_PER_LEVEL = 1000;

/**
* Calculate a user's level from their total XP.
* Formula: level = floor(xp / XP_PER_LEVEL) + 1
*/
export function calculateLevel(xpPoints: number): number {
return Math.floor(xpPoints / XP_PER_LEVEL) + 1;
}
20 changes: 20 additions & 0 deletions backend/src/config/xpConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* XP and Level Configuration
*
* Central source of truth for all XP values and level calculations.
* Change these values once — they propagate everywhere.
*/

/** XP awarded for solving a problem */
export const SOLVE_XP = 25;

/** XP required per level */
export const XP_PER_LEVEL = 1000;

/**
* Calculate a user's level from their total XP.
* Formula: level = floor(xp / XP_PER_LEVEL) + 1
*/
export function calculateLevel(xpPoints: number): number {
return Math.floor(xpPoints / XP_PER_LEVEL) + 1;
}
5 changes: 3 additions & 2 deletions backend/src/controllers/userActionController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import { prisma } from '../config/db';
import { SOLVE_XP } from '../config/xpConfig';

export const updateProblemStatus = async (req: Request | any, res: Response) => {
try {
Expand Down Expand Up @@ -75,7 +76,7 @@ export const updateProblemStatus = async (req: Request | any, res: Response) =>
await prisma.user.update({
where: { id: userId },
data: {
xp_points: { increment: 25 },
xp_points: { increment: SOLVE_XP },
solvedProblems: { push: [{ problemId, solvedAt: today }] },
streak_days: newStreak,
last_active: today,
Expand All @@ -90,7 +91,7 @@ export const updateProblemStatus = async (req: Request | any, res: Response) =>
await prisma.user.update({
where: { id: userId },
data: {
xp_points: { decrement: 25 },
xp_points: { decrement: SOLVE_XP },
solvedProblems: updatedSolvedProblems
}
});
Expand Down
5 changes: 3 additions & 2 deletions backend/src/controllers/userController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import { prisma } from '../config/db';
import { calculateLevel } from '../config/xpConfig';

// @desc Get leaderboard data
// @route GET /api/users/leaderboard
Expand Down Expand Up @@ -148,7 +149,7 @@ export const getUserProfile = async (req: Request, res: Response) => {
xp: user.xp_points || 0,
streak: user.streak_days || 0,
solved: user.solvedProblems?.length || 0,
level: Math.floor((user.xp_points || 0) / 100) + 1,
level: calculateLevel(user.xp_points || 0),
memberSince: user.createdAt,
});
} catch (error) {
Expand Down Expand Up @@ -189,7 +190,7 @@ export const updateUserProfile = async (req: Request | any, res: Response) => {
xp: updatedUser.xp_points,
streak: updatedUser.streak_days,
solved: updatedUser.solvedProblems?.length || 0,
level: Math.floor((updatedUser.xp_points || 0) / 100) + 1,
level: calculateLevel(updatedUser.xp_points || 0),
memberSince: updatedUser.createdAt,
});
} catch (error) {
Expand Down
Loading