🏆 Badge Management
View badge distribution, track awards, and manage achievement system.
Show code
Auth = window . Auth || {};
IoTClassAPI = window . IoTClassAPI || {};
// Colors
colors = ({
navy : '#2C3E50' ,
teal : '#16A085' ,
gray : '#7F8C8D'
})
// Check authentication and admin status
currentUser = {
const user = Auth. getCurrentUser ();
if (! user) return null ;
const isAdmin = Auth. isUserAdmin ();
return { ... user, isAdmin };
}
// Load badge configuration
badgeConfig = {
try {
// Try to load from BadgeSystem if available
if (typeof BadgeSystem !== 'undefined' && BadgeSystem. badgeCatalog ) {
return BadgeSystem. badgeCatalog ;
}
// Fallback: try to load badge definitions from Supabase
const definitions = await IoTClassAPI. Badge . getAllBadgeDefinitions ();
if (definitions && definitions. length > 0 ) {
return definitions;
}
return [];
} catch (error) {
console . error ('Error loading badge config:' , error);
return [];
}
}
// Load all badge awards via Supabase
allBadgeAwards = {
if (! currentUser || ! currentUser. isAdmin ) {
return null ;
}
try {
const badgeAwards = await IoTClassAPI. Admin . getAllBadges ();
// Count awards per badge
const awardCounts = {};
badgeAwards. forEach (award => {
const badgeId = award. badge_id || 'unknown' ;
awardCounts[badgeId] = (awardCounts[badgeId] || 0 ) + 1 ;
});
return {
awards : badgeAwards,
counts : awardCounts,
total : badgeAwards. length ,
uniqueRecipients : new Set (badgeAwards. map (a => a. user_id )). size
};
} catch (error) {
console . error ('Error loading badge awards:' , error);
return null ;
}
}
// Calculate badge statistics
badgeStats = {
if (! badgeConfig || ! allBadgeAwards) return null ;
const totalBadges = badgeConfig. length ;
const totalAwarded = allBadgeAwards. total ;
const avgAwardsPerBadge = totalBadges > 0 ? (totalAwarded / totalBadges). toFixed (1 ) : 0 ;
// Most awarded badge
const sortedBadges = Object . entries (allBadgeAwards. counts )
. sort ((a, b) => b[1 ] - a[1 ]);
const mostAwardedBadgeId = sortedBadges. length > 0 ? sortedBadges[0 ][0 ] : null ;
const mostAwardedBadge = badgeConfig. find (b => b. id === mostAwardedBadgeId);
const mostAwardedCount = sortedBadges. length > 0 ? sortedBadges[0 ][1 ] : 0 ;
// Least awarded (but awarded at least once)
const awardedBadges = sortedBadges. filter (([id, count]) => count > 0 );
const leastAwardedBadgeId = awardedBadges. length > 0 ? awardedBadges[awardedBadges. length - 1 ][0 ] : null ;
const leastAwardedBadge = badgeConfig. find (b => b. id === leastAwardedBadgeId);
const leastAwardedCount = awardedBadges. length > 0 ? awardedBadges[awardedBadges. length - 1 ][1 ] : 0 ;
// Badges never awarded
const neverAwarded = badgeConfig. filter (badge => ! allBadgeAwards. counts [badge. id ]);
return {
totalBadges,
totalAwarded,
avgAwardsPerBadge,
mostAwardedBadge : mostAwardedBadge ? mostAwardedBadge. name : 'None' ,
mostAwardedCount,
leastAwardedBadge : leastAwardedBadge ? leastAwardedBadge. name : 'None' ,
leastAwardedCount,
neverAwarded : neverAwarded. length ,
uniqueRecipients : allBadgeAwards. uniqueRecipients
};
}
// Filter state
viewState = ({ filterCategory : 'all' , sortBy : 'awarded-desc' })
// Filtered and sorted badges
filteredBadges = {
if (! badgeConfig) return [];
let filtered = [... badgeConfig];
// Apply category filter
if (viewState. filterCategory !== 'all' ) {
filtered = filtered. filter (badge => badge. category === viewState. filterCategory );
}
// Add award counts
filtered = filtered. map (badge => ({
... badge,
awardCount : (allBadgeAwards?. counts [badge. id ] || 0 )
}));
// Apply sorting
filtered. sort ((a, b) => {
switch (viewState. sortBy ) {
case 'awarded-desc' :
return b. awardCount - a. awardCount ;
case 'awarded-asc' :
return a. awardCount - b. awardCount ;
case 'name-asc' :
return a. name . localeCompare (b. name );
case 'name-desc' :
return b. name . localeCompare (a. name );
case 'xp-desc' :
return (b. xp_reward || 0 ) - (a. xp_reward || 0 );
case 'xp-asc' :
return (a. xp_reward || 0 ) - (b. xp_reward || 0 );
default :
return 0 ;
}
});
return filtered;
}
Show code
// Render access denied if not admin
html ` ${ ! Auth. isUserAuthenticated () || ! currentUser?. isAdmin ? `
<div class="access-denied">
<div style="font-size: 72px; margin-bottom: 20px;">🔒</div>
<h2 style="color: ${ colors. navy } ; margin-bottom: 16px;">Access Denied</h2>
<p style="color: ${ colors. gray } ;">
You must be an administrator to access this page.
</p>
</div>
` : '' } `
Show code
// Render badge management if admin
html ` ${ currentUser?. isAdmin && badgeConfig && allBadgeAwards && badgeStats ? `
<!-- Badge Statistics -->
<div class="badge-stats">
<div class="badge-stat-card">
<div class="badge-stat-value"> ${ badgeStats. totalBadges } </div>
<div class="badge-stat-label">Total Badges</div>
</div>
<div class="badge-stat-card">
<div class="badge-stat-value" style="color: ${ colors. teal } ;"> ${ badgeStats. totalAwarded } </div>
<div class="badge-stat-label">Total Awarded</div>
</div>
<div class="badge-stat-card">
<div class="badge-stat-value"> ${ badgeStats. avgAwardsPerBadge } </div>
<div class="badge-stat-label">Avg per Badge</div>
</div>
<div class="badge-stat-card">
<div class="badge-stat-value"> ${ badgeStats. uniqueRecipients } </div>
<div class="badge-stat-label">Unique Recipients</div>
</div>
<div class="badge-stat-card">
<div class="badge-stat-value" style="color: #E74C3C;"> ${ badgeStats. neverAwarded } </div>
<div class="badge-stat-label">Never Awarded</div>
</div>
</div>
<!-- Most/Least Awarded -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 30px 0;">
<div style="background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 14px; color: ${ colors. gray } ; margin-bottom: 8px;">MOST AWARDED</div>
<div style="font-size: 20px; font-weight: 600; color: ${ colors. navy } ;"> ${ badgeStats. mostAwardedBadge } </div>
<div style="font-size: 28px; font-weight: bold; color: ${ colors. teal } ; margin-top: 8px;">
${ badgeStats. mostAwardedCount } awards
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 14px; color: ${ colors. gray } ; margin-bottom: 8px;">LEAST AWARDED</div>
<div style="font-size: 20px; font-weight: 600; color: ${ colors. navy } ;"> ${ badgeStats. leastAwardedBadge } </div>
<div style="font-size: 28px; font-weight: bold; color: #E67E22; margin-top: 8px;">
${ badgeStats. leastAwardedCount } awards
</div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<select
class="filter-select"
value=" ${ viewState. filterCategory } "
onchange="viewState = { ...viewState, filterCategory: this.value }"
>
<option value="all">All Categories</option>
<option value="learning-path">Learning Path</option>
<option value="engagement">Engagement</option>
<option value="milestone">Milestone</option>
</select>
<select
class="filter-select"
value=" ${ viewState. sortBy } "
onchange="viewState = { ...viewState, sortBy: this.value }"
>
<option value="awarded-desc">Most Awarded First</option>
<option value="awarded-asc">Least Awarded First</option>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="xp-desc">Highest XP First</option>
<option value="xp-asc">Lowest XP First</option>
</select>
</div>
<!-- Badge Catalog -->
<div class="badge-catalog">
${ filteredBadges. map (badge => `
<div class="badge-item">
<div class="badge-icon"> ${ badge. emoji || '🏆' } </div>
<div class="badge-name"> ${ badge. name } </div>
<span class="badge-category category- ${ badge. category } ">
${ badge. category . replace ('-' , ' ' )}
</span>
<span class="rarity-indicator rarity- ${ badge. rarity } ">
${ badge. rarity }
</span>
<div class="badge-description"> ${ badge. description } </div>
<div class="badge-meta">
<div>
<div class="badge-awarded"> ${ badge. awardCount } </div>
<div style="font-size: 12px; color: ${ colors. gray } ;">Awards</div>
</div>
<div class="badge-xp">+ ${ badge. xp_reward || 0 } XP</div>
</div>
</div>
` ). join ('' )}
</div>
<div style="text-align: center; color: ${ colors. gray } ; font-size: 14px; margin-top: 20px;">
Showing ${ filteredBadges. length } badges
</div>
` : '' } `
Badge Management Tips
Badge Categories : - Learning Path : Awarded for completing parts or achieving learning milestones - Engagement : Awarded for active participation (games, labs, quizzes) - Milestone : Awarded for reaching XP or streak milestones
Rarity Levels : - Common : Easy to achieve, frequent awards - Uncommon : Moderate difficulty - Rare : Difficult to achieve - Legendary : Very rare, exceptional achievement
Future Features (Coming Soon): - Manual badge award tool for admins - Badge revocation (if awarded in error) - Custom badge creation - Badge expiration/renewal system