feat: implement comprehensive gamification, analytics, and leaderboard system
This commit adds a complete gamification system with analytics dashboards, leaderboards, and enhanced badge tracking functionality. Backend Features: - Analytics API with overview, user stats, activity trends, top contributors, and street statistics endpoints - Leaderboard API supporting global, weekly, monthly, and friends views - Profile API for viewing and managing user profiles - Enhanced gamification service with badge progress tracking and user stats - Comprehensive test coverage for analytics and leaderboard endpoints - Profile validation middleware for secure profile updates Frontend Features: - Analytics dashboard with multiple tabs (Overview, Activity, Personal Stats) - Interactive charts for activity trends and street statistics - Leaderboard component with pagination and timeframe filtering - Badge collection display with progress tracking - Personal stats component showing user achievements - Contributors list for top performing users - Profile management components (View/Edit) - Toast notifications integrated throughout - Comprehensive test coverage for Leaderboard component Enhancements: - User model enhanced with stats tracking and badge management - Fixed express.Router() capitalization bug in users route - Badge service improvements for better criteria matching - Removed unused imports in Profile component This feature enables users to track their contributions, view community analytics, compete on leaderboards, and earn badges for achievements. 🤖 Generated with OpenCode Co-Authored-By: AI Assistant <noreply@opencode.ai>
This commit is contained in:
200
backend/routes/leaderboard.js
Normal file
200
backend/routes/leaderboard.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const auth = require("../middleware/auth");
|
||||
const { getCacheMiddleware, invalidateCacheByPattern } = require("../middleware/cache");
|
||||
const gamificationService = require("../services/gamificationService");
|
||||
const User = require("../models/User");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/global
|
||||
* @desc Get global leaderboard (all time)
|
||||
* @access Public
|
||||
* @query limit (default 100), offset (default 0)
|
||||
*/
|
||||
router.get("/global", getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
logger.info("Fetching global leaderboard", { limit, offset });
|
||||
|
||||
const leaderboard = await gamificationService.getGlobalLeaderboard(limit, offset);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: leaderboard.length,
|
||||
limit,
|
||||
offset,
|
||||
data: leaderboard
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching global leaderboard", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching global leaderboard",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/weekly
|
||||
* @desc Get weekly leaderboard
|
||||
* @access Public
|
||||
* @query limit (default 100), offset (default 0)
|
||||
*/
|
||||
router.get("/weekly", getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
logger.info("Fetching weekly leaderboard", { limit, offset });
|
||||
|
||||
const leaderboard = await gamificationService.getWeeklyLeaderboard(limit, offset);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: leaderboard.length,
|
||||
limit,
|
||||
offset,
|
||||
timeframe: "week",
|
||||
data: leaderboard
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching weekly leaderboard", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching weekly leaderboard",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/monthly
|
||||
* @desc Get monthly leaderboard
|
||||
* @access Public
|
||||
* @query limit (default 100), offset (default 0)
|
||||
*/
|
||||
router.get("/monthly", getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
logger.info("Fetching monthly leaderboard", { limit, offset });
|
||||
|
||||
const leaderboard = await gamificationService.getMonthlyLeaderboard(limit, offset);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: leaderboard.length,
|
||||
limit,
|
||||
offset,
|
||||
timeframe: "month",
|
||||
data: leaderboard
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monthly leaderboard", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching monthly leaderboard",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/friends
|
||||
* @desc Get friends leaderboard (requires auth)
|
||||
* @access Private
|
||||
* @query limit (default 100), offset (default 0)
|
||||
*/
|
||||
router.get("/friends", auth, getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
const userId = req.user.id;
|
||||
|
||||
logger.info("Fetching friends leaderboard", { userId, limit, offset });
|
||||
|
||||
const leaderboard = await gamificationService.getFriendsLeaderboard(userId, limit, offset);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: leaderboard.length,
|
||||
limit,
|
||||
offset,
|
||||
data: leaderboard
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching friends leaderboard", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching friends leaderboard",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/user/:userId
|
||||
* @desc Get user's rank and position in leaderboard
|
||||
* @access Public
|
||||
*/
|
||||
router.get("/user/:userId", getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const timeframe = req.query.timeframe || "all"; // all, week, month
|
||||
|
||||
logger.info("Fetching user leaderboard position", { userId, timeframe });
|
||||
|
||||
const userPosition = await gamificationService.getUserLeaderboardPosition(userId, timeframe);
|
||||
|
||||
if (!userPosition) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
msg: "User not found or has no points"
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: userPosition
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching user leaderboard position", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching user position",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/leaderboard/stats
|
||||
* @desc Get leaderboard statistics
|
||||
* @access Public
|
||||
*/
|
||||
router.get("/stats", getCacheMiddleware(300), async (req, res) => {
|
||||
try {
|
||||
logger.info("Fetching leaderboard statistics");
|
||||
|
||||
const stats = await gamificationService.getLeaderboardStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching leaderboard statistics", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Server error fetching leaderboard statistics",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user