Files
adopt-a-street/backend/routes/leaderboard.js
William Valentin 3e4c730860 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>
2025-11-03 13:53:48 -08:00

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;