๐
Badge System
Earn digital badges by completing chapters, mastering skills, and achieving milestones. All badges are Open Badges 2.0 compatible and can be shared on LinkedIn!
Digital badges are verifiable credentials that recognize your learning achievements. Each badge includes:
Badge Image (3 style variants: geometric, modern, artistic)
Criteria (what you need to do to earn it)
XP Reward (experience points awarded)
Open Badges 2.0 JSON (for portfolio/LinkedIn)
Show code
Auth = window . Auth || {};
IoTClassAPI = window . IoTClassAPI || {};
BadgeSystem = window . BadgeSystem || {};
// Colors
colors = ({
navy : '#2C3E50' ,
teal : '#16A085' ,
orange : '#E67E22' ,
gray : '#7F8C8D' ,
lightGray : '#ECF0F1'
})
// Check authentication
currentUser = {
const user = Auth. getCurrentUser ();
if (! user) {
return null ;
}
// Get user profile from Supabase
const userData = await IoTClassAPI. User . getProfile (user. id );
return {
... user,
... userData
};
}
// Badge catalog (all 15 badges)
badgeCatalog = {
await BadgeSystem. init ();
return BadgeSystem. badgeCatalog ;
}
// User's earned badges - Using Supabase via BadgeSystem or direct API
userBadges = {
if (! Auth. isUserAuthenticated ()) {
return [];
}
try {
// Try BadgeSystem first (it may have more details)
if (BadgeSystem. getUserBadgesWithDetails ) {
const badges = await BadgeSystem. getUserBadgesWithDetails (currentUser. id );
return badges;
}
// Fallback to direct Supabase API
const badges = await IoTClassAPI. Badge . getUserBadges (currentUser. id );
return badges. map (b => ({
... b,
badgeId : b. badge_id ,
earnedDate : b. earned_at
}));
} catch (error) {
console . error ('Error loading user badges:' , error);
return [];
}
}
// Badge progress statistics
badgeStats = {
const earnedCount = userBadges. length ;
const totalCount = badgeCatalog. length ;
const completionPercent = Math . round ((earnedCount / totalCount) * 100 );
// Count by category
const byCategory = {
'learning-path' : { earned : 0 , total : 0 },
'engagement' : { earned : 0 , total : 0 },
'milestone' : { earned : 0 , total : 0 }
};
badgeCatalog. forEach (badge => {
byCategory[badge. category ]. total ++;
});
userBadges. forEach (badge => {
byCategory[badge. category ]. earned ++;
});
// Total XP from badges
const totalBadgeXP = userBadges. reduce ((sum, badge) => sum + (badge. xp_reward || 0 ), 0 );
return {
earnedCount,
totalCount,
completionPercent,
byCategory,
totalBadgeXP
};
}
// Filter state
viewState = {
return {
view : 'all' , // all, earned, not-earned
categoryFilter : 'all' // all, learning-path, engagement, milestone
};
}
// Filtered badges
filteredBadges = {
let badges = badgeCatalog;
// Apply view filter
if (viewState. view === 'earned' ) {
badges = badges. filter (badge =>
userBadges. some (earned => earned. badgeId === badge. id )
);
} else if (viewState. view === 'not-earned' ) {
badges = badges. filter (badge =>
! userBadges. some (earned => earned. badgeId === badge. id )
);
}
// Apply category filter
if (viewState. categoryFilter !== 'all' ) {
badges = badges. filter (badge => badge. category === viewState. categoryFilter );
}
// Merge with earned data
return badges. map (badge => {
const earned = userBadges. find (e => e. badgeId === badge. id );
return {
... badge,
earned : !! earned,
earnedDate : earned?. earnedDate
};
});
}
Show code
// Render login prompt if not authenticated
html ` ${ ! Auth. isUserAuthenticated () ? `
<div style="
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<div style="font-size: 64px; margin-bottom: 20px;">๐</div>
<h2 style="color: ${ colors. navy } ; margin-bottom: 16px;">Sign in to Earn Badges</h2>
<p style="color: ${ colors. gray } ; margin-bottom: 24px;">
Track your achievements and earn digital credentials!
</p>
<button
onclick="Auth.login()"
style="
padding: 12px 32px;
background: ${ colors. teal } ;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
"
>
<i class="fab fa-github"></i> Sign in with GitHub
</button>
</div>
` : '' } `
Show code
// Render badge progress section
html ` ${ Auth. isUserAuthenticated () ? `
<div class="progress-section">
<h3 style="margin: 0 0 24px 0; color: ${ colors. navy } ;">Your Badge Progress</h3>
<div class="progress-stats">
<div class="progress-stat">
<div class="progress-stat-value"> ${ badgeStats. earnedCount } / ${ badgeStats. totalCount } </div>
<div class="progress-stat-label">Badges Earned</div>
</div>
<div class="progress-stat">
<div class="progress-stat-value"> ${ badgeStats. completionPercent } %</div>
<div class="progress-stat-label">Completion</div>
</div>
<div class="progress-stat">
<div class="progress-stat-value"> ${ badgeStats. totalBadgeXP . toLocaleString ()} </div>
<div class="progress-stat-label">XP from Badges</div>
</div>
<div class="progress-stat">
<div class="progress-stat-value">
${ badgeStats. byCategory ['learning-path' ]. earned } / ${ badgeStats. byCategory ['learning-path' ]. total }
</div>
<div class="progress-stat-label">Learning Path</div>
</div>
<div class="progress-stat">
<div class="progress-stat-value">
${ badgeStats. byCategory ['engagement' ]. earned } / ${ badgeStats. byCategory ['engagement' ]. total }
</div>
<div class="progress-stat-label">Engagement</div>
</div>
<div class="progress-stat">
<div class="progress-stat-value">
${ badgeStats. byCategory ['milestone' ]. earned } / ${ badgeStats. byCategory ['milestone' ]. total }
</div>
<div class="progress-stat-label">Milestones</div>
</div>
</div>
<!-- View Tabs -->
<div class="badge-tabs">
<button class="badge-tab ${ viewState. view === 'all' ? 'active' : '' } " onclick="viewState.view = 'all'">
All Badges ( ${ badgeCatalog. length } )
</button>
<button class="badge-tab ${ viewState. view === 'earned' ? 'active' : '' } " onclick="viewState.view = 'earned'">
Earned ( ${ badgeStats. earnedCount } )
</button>
<button class="badge-tab ${ viewState. view === 'not-earned' ? 'active' : '' } " onclick="viewState.view = 'not-earned'">
Not Earned ( ${ badgeCatalog. length - badgeStats. earnedCount } )
</button>
</div>
<!-- Category Filters -->
<div class="category-filter">
<button class="filter-btn ${ viewState. categoryFilter === 'all' ? 'active' : '' } " onclick="viewState.categoryFilter = 'all'">
All Categories
</button>
<button class="filter-btn ${ viewState. categoryFilter === 'learning-path' ? 'active' : '' } " onclick="viewState.categoryFilter = 'learning-path'">
๐ Learning Path
</button>
<button class="filter-btn ${ viewState. categoryFilter === 'engagement' ? 'active' : '' } " onclick="viewState.categoryFilter = 'engagement'">
๐ฎ Engagement
</button>
<button class="filter-btn ${ viewState. categoryFilter === 'milestone' ? 'active' : '' } " onclick="viewState.categoryFilter = 'milestone'">
โญ Milestones
</button>
</div>
</div>
` : '' } `
Show code
// Render badges grid
html ` ${ Auth. isUserAuthenticated () ? `
<div class="badges-grid">
${ filteredBadges. map (badge => {
const rarityClass = `rarity- ${ badge. rarity } ` ;
const categoryClass = `category- ${ badge. category } ` ;
return `
<div class="badge-card ${ badge. earned ? 'earned' : 'locked' } ">
<div class="badge-emoji ${ ! badge. earned ? 'badge-locked-emoji' : '' } ">
${ badge. emoji }
</div>
<h3 class="badge-name"> ${ badge. name } </h3>
<div class="badge-category ${ categoryClass} ">
${ badge. category . replace ('-' , ' ' )}
</div>
<div class="badge-rarity ${ rarityClass} ">
${ badge. rarity }
</div>
<div class="badge-xp-reward">
+ ${ badge. xp_reward } XP
</div>
<p class="badge-criteria">
${ badge. earning_logic . criteria || badge. criteria || 'Complete the requirements to earn this badge' }
</p>
${ badge. earned ? `
<div class="badge-earned-date">
Earned: ${ new Date (badge. earnedDate ). toLocaleDateString ('en-US' , { year : 'numeric' , month : 'long' , day : 'numeric' })}
</div>
<div class="badge-actions">
<button
class="badge-action-btn btn-download"
onclick="BadgeSystem.downloadBadge(' ${ currentUser. id } ', ' ${ badge. id } ', 'modern')"
>
๐ฅ Download
</button>
<button
class="badge-action-btn btn-share"
onclick="BadgeSystem.shareBadgeOnLinkedIn(' ${ badge. id } ')"
>
<i class="fab fa-linkedin"></i> Share
</button>
<button
class="badge-action-btn btn-json"
onclick="BadgeSystem.downloadBadgeJSON(' ${ currentUser. id } ', ' ${ badge. id } ')"
>
{} JSON
</button>
</div>
` : `
<div style="color: ${ colors. gray } ; font-size: 14px; margin-top: 12px;">
๐ Not earned yet
</div>
` }
</div>
` ;
}). join ('' )}
</div>
` : '' } `
Badge Categories
๐ Learning Path Badges (5)
Earn these badges by completing major curriculum sections:
IoT Fundamentals - Complete Part 2 (6 chapters)
Architecture Master - Complete Parts 4-5 (73 chapters)
Networking Expert - Complete Parts 7-9 (114 chapters)
Security Specialist - Complete Part 11 (14 chapters)
IoT Full Stack - Complete all 14 parts (271+ chapters)
๐ฎ Engagement Badges (5)
Earn these badges through active participation:
Game Champion - Complete 10 educational games with 90%+ score
Lab Technician - Complete 20 Wokwi labs
Quiz Master - Answer 100 knowledge checks correctly
Sensor Squad Friend - Read all 108 Sensor Squad sections
Tool Explorer - Use 25 interactive OJS tools
โญ Milestone Badges (5)
Earn these badges by achieving significant milestones:
First Steps - Complete your first chapter
Week Warrior - Maintain a 7-day learning streak
Perfectionist - Score 100% on 5 consecutive knowledge checks
Graduate - Earn 10,000 XP
Legend - Earn 50,000 XP
Open Badges 2.0
All IoT Class badges are Open Badges 2.0 compliant. Each badge includes:
Badge Image (SVG format, 3 style variants)
Badge Metadata (JSON format with issuer, criteria, evidence)
Verifiable (hosted assertion for verification)
Shareable (on LinkedIn, portfolio, resume)
Download : Click โDownloadโ to save the badge image (SVG)
Share on LinkedIn : Click โShareโ to post to your LinkedIn profile
Get JSON : Click โJSONโ to download Open Badges 2.0 metadata
Add to Portfolio : Use the hosted JSON URL for your digital portfolio
Badge Rarity Levels
Common
Gray
Easy to earn
60-70%
Uncommon
Green
Moderate effort
20-30%
Rare
Blue
Significant achievement
5-10%
Legendary
Gold
Exceptional mastery
<1%