diff --git a/app/src/sections/DailyChallenges.tsx b/app/src/sections/DailyChallenges.tsx
index 188e23d..466f552 100644
--- a/app/src/sections/DailyChallenges.tsx
+++ b/app/src/sections/DailyChallenges.tsx
@@ -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;
@@ -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 => {
@@ -205,7 +206,7 @@ export function DailyChallenges({ onBack }: DailyChallengesProps) {
- {challengesSolved * 25} XP Earned
+ {challengesSolved * SOLVE_XP} XP Earned
@@ -309,7 +310,7 @@ export function DailyChallenges({ onBack }: DailyChallengesProps) {
- {isCompleted ? 'Earned' : 'Reward'}: +25 XP
+ {isCompleted ? 'Earned' : 'Reward'}: +{SOLVE_XP} XP
diff --git a/app/src/sections/Dashboard.tsx b/app/src/sections/Dashboard.tsx
index b546cba..e4923a8 100644
--- a/app/src/sections/Dashboard.tsx
+++ b/app/src/sections/Dashboard.tsx
@@ -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;
@@ -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;
@@ -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 (
@@ -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)' },
@@ -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 && (
+
+
+
?
+
+ {stat.tooltip}
+
+
{isRefreshing && (
@@ -831,7 +844,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
- +25 XP
+ +{SOLVE_XP} XP
);
}) : (
diff --git a/app/src/sections/Problems.tsx b/app/src/sections/Problems.tsx
index 727622c..f13c879 100644
--- a/app/src/sections/Problems.tsx
+++ b/app/src/sections/Problems.tsx
@@ -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({
@@ -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) {
diff --git a/app/src/sections/TopicDetail.tsx b/app/src/sections/TopicDetail.tsx
index 1f3d75e..3d83135 100644
--- a/app/src/sections/TopicDetail.tsx
+++ b/app/src/sections/TopicDetail.tsx
@@ -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;
@@ -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) {
diff --git a/app/src/utils/xpConfig.ts b/app/src/utils/xpConfig.ts
new file mode 100644
index 0000000..47f6f55
--- /dev/null
+++ b/app/src/utils/xpConfig.ts
@@ -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;
+}
diff --git a/backend/src/config/xpConfig.ts b/backend/src/config/xpConfig.ts
new file mode 100644
index 0000000..47f6f55
--- /dev/null
+++ b/backend/src/config/xpConfig.ts
@@ -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;
+}
diff --git a/backend/src/controllers/userActionController.ts b/backend/src/controllers/userActionController.ts
index 7f36924..798149c 100644
--- a/backend/src/controllers/userActionController.ts
+++ b/backend/src/controllers/userActionController.ts
@@ -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 {
@@ -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,
@@ -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
}
});
diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts
index 7aeefc9..afed119 100644
--- a/backend/src/controllers/userController.ts
+++ b/backend/src/controllers/userController.ts
@@ -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
@@ -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) {
@@ -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) {