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:
126
backend/routes/profile.js
Normal file
126
backend/routes/profile.js
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user