Files
adopt-a-street/backend/middleware/validators/profileValidator.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

53 lines
1.5 KiB
JavaScript

const { body, validationResult } = require("express-validator");
const URL_REGEX = /^(https?|ftp):\\/\\/[^\\s\\/$.?#].[^\\s]*$/i;
const validateProfile = [
body("bio")
.optional()
.isLength({ max: 500 })
.withMessage("Bio cannot exceed 500 characters."),
body("location").optional().isString(),
body("website")
.optional()
.if(body("website").notEmpty())
.matches(URL_REGEX)
.withMessage("Invalid website URL."),
body("social.twitter")
.optional()
.if(body("social.twitter").notEmpty())
.matches(URL_REGEX)
.withMessage("Invalid Twitter URL."),
body("social.github")
.optional()
.if(body("social.github").notEmpty())
.matches(URL_REGEX)
.withMessage("Invalid Github URL."),
body("social.linkedin")
.optional()
.if(body("social.linkedin").notEmpty())
.matches(URL_REGEX)
.withMessage("Invalid LinkedIn URL."),
body("privacySettings.profileVisibility")
.optional()
.isIn(["public", "private"])
.withMessage("Profile visibility must be public or private."),
body("preferences.emailNotifications").optional().isBoolean(),
body("preferences.pushNotifications").optional().isBoolean(),
body("preferences.theme")
.optional()
.isIn(["light", "dark"])
.withMessage("Theme must be light or dark."),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
module.exports = { validateProfile };