Files
adopt-a-street/frontend/src/components/BadgeProgress.js
T
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

80 lines
2.6 KiB
JavaScript

import React from "react";
import PropTypes from "prop-types";
/**
* BadgeProgress component - displays progress bars for badges in progress
* @param {Array} badges - Array of badge objects with progress information
*/
const BadgeProgress = ({ badges }) => {
// Filter to show only badges that are in progress (not earned and have some progress)
const inProgressBadges = badges.filter(
(badge) => !badge.isEarned && badge.progress > 0 && badge.threshold > 0
);
if (inProgressBadges.length === 0) {
return (
<div className="alert alert-info">
<p className="mb-0">
Complete tasks and participate in events to earn badges!
</p>
</div>
);
}
return (
<div className="badge-progress-container">
<h5 className="mb-3">Badges In Progress</h5>
{inProgressBadges.map((badge) => {
const percentage = Math.round((badge.progress / badge.threshold) * 100);
return (
<div key={badge._id} className="mb-3">
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<span style={{ fontSize: "1.5rem", marginRight: "0.5rem" }}>
{badge.icon || "🏆"}
</span>
<div>
<strong>{badge.name}</strong>
<br />
<small className="text-muted">{badge.description}</small>
</div>
</div>
<span className="badge badge-info">
{badge.progress} / {badge.threshold}
</span>
</div>
<div className="progress" style={{ height: "20px" }}>
<div
className={`progress-bar ${percentage >= 75 ? "bg-success" : percentage >= 50 ? "bg-info" : "bg-warning"}`}
role="progressbar"
style={{ width: `${percentage}%` }}
aria-valuenow={badge.progress}
aria-valuemin="0"
aria-valuemax={badge.threshold}
>
{percentage}%
</div>
</div>
</div>
);
})}
</div>
);
};
BadgeProgress.propTypes = {
badges: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
icon: PropTypes.string,
progress: PropTypes.number.isRequired,
threshold: PropTypes.number.isRequired,
isEarned: PropTypes.bool.isRequired,
})
).isRequired,
};
export default BadgeProgress;