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>
201 lines
5.4 KiB
JavaScript
201 lines
5.4 KiB
JavaScript
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;
|