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>
127 lines
3.2 KiB
JavaScript
127 lines
3.2 KiB
JavaScript
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;
|
|
|