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:
William Valentin
2025-11-03 13:53:48 -08:00
parent ae77e30ffb
commit 3e4c730860
34 changed files with 5533 additions and 190 deletions

126
backend/routes/profile.js Normal file
View File

@@ -0,0 +1,126 @@
const express = require("express");
const User = require("../models/User");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const { upload, handleUploadError } = require("../middleware/upload");
const { uploadImage, deleteImage } = require("../config/cloudinary");
const { validateProfile } = require("../middleware/validators/profileValidator");
const { userIdValidation } = require("../middleware/validators/userValidator");
const router = express.Router();
// GET user profile
router.get(
"/:userId",
auth,
userIdValidation,
asyncHandler(async (req, res) => {
const { userId } = req.params;
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
if (user.privacySettings.profileVisibility === "private" && req.user.id !== userId) {
return res.status(403).json({ msg: "This profile is private" });
}
res.json(user.toSafeObject());
})
);
// PUT update user profile
router.put(
"/",
auth,
validateProfile,
asyncHandler(async (req, res) => {
const userId = req.user.id;
const {
bio,
location,
website,
social,
privacySettings,
preferences,
} = req.body;
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
// Update fields
if (bio !== undefined) user.bio = bio;
if (location !== undefined) user.location = location;
if (website !== undefined) user.website = website;
if (social !== undefined) user.social = { ...user.social, ...social };
if (privacySettings !== undefined) user.privacySettings = { ...user.privacySettings, ...privacySettings };
if (preferences !== undefined) user.preferences = { ...user.preferences, ...preferences };
const updatedUser = await user.save();
res.json(updatedUser.toSafeObject());
})
);
// POST upload avatar
router.post(
"/avatar",
auth,
upload.single("avatar"),
handleUploadError,
asyncHandler(async (req, res) => {
if (!req.file) {
return res.status(400).json({ msg: "No image file provided" });
}
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
if (user.cloudinaryPublicId) {
await deleteImage(user.cloudinaryPublicId);
}
const result = await uploadImage(
req.file.buffer,
"adopt-a-street/avatars"
);
user.avatar = result.secure_url;
user.cloudinaryPublicId = result.public_id;
const updatedUser = await user.save();
res.json({
msg: "Avatar updated successfully",
avatar: updatedUser.avatar
});
})
);
// DELETE remove avatar
router.delete(
"/avatar",
auth,
asyncHandler(async (req, res) => {
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
if (user.cloudinaryPublicId) {
await deleteImage(user.cloudinaryPublicId);
user.avatar = null;
user.cloudinaryPublicId = null;
await user.save();
}
res.json({ msg: "Avatar removed successfully" });
})
);
module.exports = router;