Leaderboard
Track your progress and see how you rank among the IoT Class community!
The leaderboard is opt-in only . Your username and stats will only appear if you choose to participate. You can opt out at any time.
What’s shown : Username (anonymous), XP, Level, Badges
What’s NOT shown : Email, real name, specific chapter progress
Show code
colors = ({
navy : '#2C3E50' ,
teal : '#16A085' ,
orange : '#E67E22' ,
gray : '#7F8C8D' ,
lightGray : '#ECF0F1' ,
darkGray : '#34495E' ,
green : '#27AE60' ,
gold : '#F39C12' ,
silver : '#BDC3C7' ,
bronze : '#CD7F32' ,
red : '#E74C3C'
})
// State management
mutable loadingState = 'loading'
mutable errorMessage = null
mutable leaderboardUsers = []
mutable currentUserRank = null
mutable currentUserProfile = null
mutable isOptedIn = false
mutable isAuthenticated = false
// Initialize and load data
initLeaderboard = {
// Wait for Auth and LeaderboardAPI to be available
const waitForDeps = () => new Promise ((resolve) => {
const check = () => {
if (window . Auth && window . LeaderboardAPI && window . IoTClassSupabase ) {
resolve ();
} else {
setTimeout (check, 100 );
}
};
check ();
});
await waitForDeps ();
// Wait a bit for Auth to initialize
await new Promise (resolve => setTimeout (resolve, 500 ));
try {
// Check authentication
const authStatus = window . Auth . isUserAuthenticated ();
mutable isAuthenticated = authStatus;
if (! authStatus) {
mutable loadingState = 'not-authenticated' ;
return ;
}
// Get current user
const user = window . Auth . getCurrentUser ();
if (! user) {
mutable loadingState = 'not-authenticated' ;
return ;
}
// Load user profile and opt-in status
const [profile, optedIn] = await Promise . all ([
window . LeaderboardAPI . getUserProfile (user. id ),
window . LeaderboardAPI . isUserOptedIn (user. id )
]);
mutable currentUserProfile = profile;
mutable isOptedIn = optedIn;
// Load leaderboard data
const users = await window . LeaderboardAPI . getTopUsers (100 );
mutable leaderboardUsers = users;
// Get current user's rank if opted in
if (optedIn) {
const userRank = await window . LeaderboardAPI . getUserRank (user. id );
mutable currentUserRank = userRank;
}
mutable loadingState = 'loaded' ;
} catch (error) {
console . error ('Error loading leaderboard:' , error);
mutable errorMessage = error. message || 'Failed to load leaderboard data' ;
mutable loadingState = 'error' ;
}
}
// Toggle opt-in handler
toggleOptIn = async () => {
if (! window . Auth . isUserAuthenticated ()) return ;
const user = window . Auth . getCurrentUser ();
if (! user) return ;
const newOptInStatus = ! isOptedIn;
try {
const success = await window . LeaderboardAPI . toggleOptIn (user. id , newOptInStatus);
if (success) {
// Reload page to refresh data
window . location . reload ();
} else {
alert ('Failed to update leaderboard settings. Please try again.' );
}
} catch (error) {
console . error ('Error toggling opt-in:' , error);
alert ('Failed to update leaderboard settings. Please try again.' );
}
}
Show code
// Loading state
html ` ${ loadingState === 'loading' ? `
<div class="loading-spinner">
<div class="spinner"></div>
</div>
<p style="text-align: center; color: ${ colors. gray } ;">Loading leaderboard...</p>
` : '' } `
Show code
// Error state
html ` ${ loadingState === 'error' ? `
<div class="error-message">
<div style="font-size: 48px; margin-bottom: 16px;">:(</div>
<h3>Failed to Load Leaderboard</h3>
<p> ${ errorMessage || 'An unexpected error occurred. Please try again later.' } </p>
<button type="button" onclick="window.location.reload()" style="
margin-top: 16px;
padding: 12px 24px;
background: ${ colors. teal } ;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
">Retry</button>
</div>
` : '' } `
Show code
// Not authenticated state
html ` ${ loadingState === 'not-authenticated' ? `
<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 View Leaderboard</h2>
<p style="color: ${ colors. gray } ; margin-bottom: 24px;">
Track your progress and compete with other learners!
</p>
<button
type="button"
onclick="window.Auth.login()"
style="
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
background: ${ colors. navy } ;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Sign in with GitHub
</button>
</div>
` : '' } `
Show code
// Opt-in prompt (authenticated but not opted in)
html ` ${ loadingState === 'loaded' && ! isOptedIn ? `
<div class="opt-in-section">
<h3>Join the Leaderboard</h3>
<p style="color: ${ colors. gray } ; margin-bottom: 20px;">
Compete with other learners and track your ranking!<br>
Only your GitHub username and stats (XP, level, badges) will be visible.
</p>
<button type="button" class="opt-in-button" onclick=" ${ () => toggleOptIn ()} ">
Opt In to Leaderboard
</button>
</div>
<!-- Still show the leaderboard even if not opted in -->
<h3 style="margin-top: 40px; color: ${ colors. navy } ;">Current Rankings</h3>
<p style="color: ${ colors. gray } ; margin-bottom: 20px;">
You're not on the leaderboard. Opt in above to compete!
</p>
` : '' } `
Show code
// Main leaderboard display (authenticated and opted in)
html ` ${ loadingState === 'loaded' && isOptedIn ? `
<div>
<!-- Current User Stats -->
<div style="
background: linear-gradient(135deg, ${ colors. navy } 0%, ${ colors. darkGray } 100%);
color: white;
padding: 32px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<h3 style="margin: 0 0 16px 0; color: white;">Your Rank</h3>
<div style="display: flex; justify-content: space-around; flex-wrap: wrap; gap: 20px;">
<div style="text-align: center;">
<div style="font-size: 48px; font-weight: bold;">
${ currentUserRank ? `# ${ currentUserRank. rank } ` : 'N/A' }
</div>
<div style="opacity: 0.9;">Rank</div>
</div>
<div style="text-align: center;">
<div style="font-size: 48px; font-weight: bold;"> ${ currentUserProfile?. total_xp || 0 } </div>
<div style="opacity: 0.9;">XP</div>
</div>
<div style="text-align: center;">
<div style="font-size: 48px; font-weight: bold;"> ${ currentUserProfile?. level || 1 } </div>
<div style="opacity: 0.9;">Level</div>
</div>
<div style="text-align: center;">
<div style="font-size: 48px; font-weight: bold;"> ${ currentUserProfile?. badge_count || 0 } </div>
<div style="opacity: 0.9;">Badges</div>
</div>
</div>
</div>
<!-- Time Frame Tabs -->
<div class="leaderboard-tabs">
<button type="button" class="leaderboard-tab active">All Time</button>
<button type="button" class="leaderboard-tab" disabled>Monthly (Coming Soon)</button>
<button type="button" class="leaderboard-tab" disabled>Weekly (Coming Soon)</button>
</div>
</div>
` : '' } `
Show code
// Leaderboard table (shown for all authenticated users)
html ` ${ loadingState === 'loaded' ? `
<table class="leaderboard-table">
<thead>
<tr>
<th style="width: 80px;">Rank</th>
<th>User</th>
<th style="width: 100px;">Level</th>
<th style="width: 120px;">XP</th>
<th style="width: 100px;">Badges</th>
</tr>
</thead>
<tbody>
${ leaderboardUsers. length === 0 ? `
<tr>
<td colspan="5" style="text-align: center; padding: 40px; color: ${ colors. gray } ;">
No users on the leaderboard yet. Be the first to opt in!
</td>
</tr>
` : leaderboardUsers. map (user => {
const currentUser = window . Auth ?. getCurrentUser ();
const isCurrentUser = currentUser && user. id === currentUser. id ;
const rankClass = user. rank === 1 ? 'rank-1' : user. rank === 2 ? 'rank-2' : user. rank === 3 ? 'rank-3' : 'rank-other' ;
return `
<tr class=" ${ isCurrentUser ? 'current-user-row' : '' } ">
<td>
<div class="rank-badge ${ rankClass} ">
${ user. rank }
</div>
</td>
<td>
<div class="user-info">
<img src=" ${ user. avatar_url || 'https://via.placeholder.com/48' } " alt=" ${ user. username } " class="user-avatar">
<span class="user-name">
${ user. display_name || user. username }
${ isCurrentUser ? ' (You)' : '' }
</span>
</div>
</td>
<td>
<span class="level-badge">Level ${ user. level || 1 } </span>
</td>
<td>
<div style="font-weight: 600; color: ${ colors. navy } ; margin-bottom: 4px;">
${ (user. total_xp || 0 ). toLocaleString ()} XP
</div>
<div class="xp-bar-container">
<div class="xp-bar" style="width: ${ Math . min (((user. total_xp || 0 ) / 50000 ) * 100 , 100 )} %;"></div>
</div>
</td>
<td style="text-align: center; font-weight: 600; color: ${ colors. teal } ;">
${ user. badge_count || 0 }
</td>
</tr>
` ;
}). join ('' )}
</tbody>
</table>
` : '' } `
Show code
// Opt-out section (only shown when opted in)
html ` ${ loadingState === 'loaded' && isOptedIn ? `
<div style="text-align: center; margin-top: 40px; padding: 20px;">
<p style="color: ${ colors. gray } ; margin-bottom: 12px;">
Want to remove yourself from the leaderboard?
</p>
<button type="button" class="opt-out-button" onclick=" ${ () => toggleOptIn ()} ">
Opt Out of Leaderboard
</button>
</div>
` : '' } `
Leaderboard FAQ
Rankings are based on total XP earned . Users with higher XP appear higher on the leaderboard. In case of ties, users who reached that XP first are ranked higher.
Earn more XP by:
Completing chapters (+100 XP each)
Answering knowledge checks correctly (+10 XP each)
Completing Wokwi labs (+50 XP each)
Scoring 90%+ on educational games (+25 XP each)
Earning badges (varies by badge)
Only the following information is shown:
GitHub username (anonymous)
Total XP
Current level
Number of badges earned
Your email, real name, and specific chapter progress are never shown.
Yes! You can opt out at any time by clicking the “Opt Out of Leaderboard” button at the bottom of this page. Your data will be immediately removed from the public leaderboard.
If you don’t see your rank, it could be because:
You haven’t opted in to the leaderboard yet
You haven’t completed any chapters yet (need at least 1 chapter for XP)
You’re outside the top 100 (leaderboard shows top 100 only)
Complete more chapters to earn XP and improve your ranking!