🔧 Admin Dashboard
Welcome to the IoT Class admin portal. Manage users, content, and view platform analytics.
This page is only accessible to administrators. If you’re not an admin and believe you should have access, please contact the site administrator.
Show code
Auth = window.Auth || {};
IoTClassAPI = window.IoTClassAPI || {};
// Colors
colors = ({
navy: '#2C3E50',
teal: '#16A085',
orange: '#E67E22',
gray: '#7F8C8D',
lightGray: '#ECF0F1',
green: '#27AE60',
red: '#E74C3C',
blue: '#3498DB'
})
// Check authentication and admin status
currentUser = {
const user = Auth.getCurrentUser();
if (!user) {
return null;
}
// Check if user is admin using Auth module
const isAdmin = Auth.isUserAdmin();
return {
...user,
isAdmin: isAdmin
};
}
// Load platform statistics using Supabase
platformStats = {
if (!currentUser || !currentUser.isAdmin) {
return null;
}
try {
// Get all users via Supabase Admin API
const allUsers = await IoTClassAPI.Admin.getAllUsers();
// Get all progress records via Supabase
const allProgress = await IoTClassAPI.Admin.getAllProgress();
// Get all badges awarded via Supabase
const allBadges = await IoTClassAPI.Admin.getAllBadges();
// Get all knowledge check results via Supabase
const allKnowledgeChecks = await IoTClassAPI.Admin.getAllKnowledgeChecks();
// Calculate statistics
const totalUsers = allUsers.length;
const totalXP = allUsers.reduce((sum, user) => sum + (user.total_xp || 0), 0);
const totalBadges = allBadges.length;
const totalChaptersCompleted = allProgress.filter(p => p.completed).length;
const totalKnowledgeChecks = allKnowledgeChecks.length;
// Average XP per user
const avgXPPerUser = totalUsers > 0 ? Math.round(totalXP / totalUsers) : 0;
// Users registered in last 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const newUsersLast7Days = allUsers.filter(user => {
const joinDate = new Date(user.created_at);
return joinDate >= sevenDaysAgo;
}).length;
// Active users (activity in last 7 days)
const activeUsers = allUsers.filter(user => {
if (!user.updated_at) return false;
const lastActive = new Date(user.updated_at);
return lastActive >= sevenDaysAgo;
}).length;
// Most popular chapters (by completion count)
const chapterCompletions = {};
allProgress.forEach(p => {
const chapter = p.chapter_path || 'unknown';
chapterCompletions[chapter] = (chapterCompletions[chapter] || 0) + 1;
});
const popularChapters = Object.entries(chapterCompletions)
.map(([chapter, count]) => ({ chapter, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Recent user registrations (last 10)
const recentUsers = [...allUsers]
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 10);
// Badge distribution
const badgeTypes = {};
allBadges.forEach(b => {
const badgeName = b.badge_name || 'Unknown';
badgeTypes[badgeName] = (badgeTypes[badgeName] || 0) + 1;
});
return {
totalUsers,
totalXP,
totalBadges,
totalChaptersCompleted,
totalKnowledgeChecks,
avgXPPerUser,
newUsersLast7Days,
activeUsers,
popularChapters,
recentUsers,
badgeTypes
};
} catch (error) {
console.error('Error loading platform stats:', error);
return null;
}
}
Show code
// Render access denied if not admin
html`${!Auth.isUserAuthenticated() || !currentUser?.isAdmin ? `
<div class="access-denied">
<div class="access-denied-icon">🔒</div>
<h2 style="color: ${colors.navy}; margin-bottom: 16px;">Access Denied</h2>
<p style="color: ${colors.gray}; margin-bottom: 24px;">
You must be an administrator to access this page.
</p>
${!Auth.isUserAuthenticated() ? `
<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 loading spinner while fetching data
html`${currentUser?.isAdmin && !platformStats ? `
<div class="loading-spinner">
<div class="loading-spinner-icon">⏳</div>
<p style="color: ${colors.gray};">Loading platform statistics...</p>
</div>
` : ''}`
Show code
// Render admin dashboard
html`${currentUser?.isAdmin && platformStats ? `
<!-- Statistics Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-value">${platformStats.totalUsers.toLocaleString()}</div>
<div class="stat-label">Total Users</div>
<div class="stat-change positive">+${platformStats.newUsersLast7Days} last 7 days</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚡</div>
<div class="stat-value">${platformStats.totalXP.toLocaleString()}</div>
<div class="stat-label">Total XP Earned</div>
<div class="stat-change">${platformStats.avgXPPerUser.toLocaleString()} avg per user</div>
</div>
<div class="stat-card">
<div class="stat-icon">🏆</div>
<div class="stat-value">${platformStats.totalBadges.toLocaleString()}</div>
<div class="stat-label">Badges Awarded</div>
<div class="stat-change">${Object.keys(platformStats.badgeTypes).length} unique badges</div>
</div>
<div class="stat-card">
<div class="stat-icon">📚</div>
<div class="stat-value">${platformStats.totalChaptersCompleted.toLocaleString()}</div>
<div class="stat-label">Chapters Completed</div>
<div class="stat-change">${(platformStats.totalChaptersCompleted / Math.max(platformStats.totalUsers, 1)).toFixed(1)} per user</div>
</div>
<div class="stat-card">
<div class="stat-icon">✅</div>
<div class="stat-value">${platformStats.totalKnowledgeChecks.toLocaleString()}</div>
<div class="stat-label">Knowledge Checks Completed</div>
<div class="stat-change">${(platformStats.totalKnowledgeChecks / Math.max(platformStats.totalUsers, 1)).toFixed(1)} per user</div>
</div>
<div class="stat-card">
<div class="stat-icon">🔥</div>
<div class="stat-value">${platformStats.activeUsers.toLocaleString()}</div>
<div class="stat-label">Active Users (7 days)</div>
<div class="stat-change">${((platformStats.activeUsers / Math.max(platformStats.totalUsers, 1)) * 100).toFixed(1)}% of total</div>
</div>
</div>
<!-- Most Popular Chapters -->
<div class="section-header">
<h3 class="section-title">📊 Most Popular Chapters</h3>
</div>
<table class="data-table">
<thead>
<tr>
<th style="width: 60px;">Rank</th>
<th>Chapter</th>
<th style="width: 120px;">Completions</th>
<th style="width: 200px;">Popularity</th>
</tr>
</thead>
<tbody>
${platformStats.popularChapters.map((chapter, index) => {
const maxCompletions = platformStats.popularChapters[0].count;
const percentage = (chapter.count / maxCompletions) * 100;
const chapterName = chapter.chapter.split('/').pop().replace('.qmd', '').replace(/-/g, ' ');
return `
<tr>
<td style="font-weight: 600; color: ${colors.navy};">#${index + 1}</td>
<td>
<div style="font-weight: 500;">${chapterName}</div>
<div style="font-size: 12px; color: ${colors.gray};">${chapter.chapter}</div>
</td>
<td style="text-align: center; font-weight: 600; color: ${colors.teal};">${chapter.count}</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${percentage}%;"></div>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- Recent User Registrations -->
<div class="section-header">
<h3 class="section-title">🆕 Recent User Registrations</h3>
</div>
<table class="data-table">
<thead>
<tr>
<th>User</th>
<th>Email</th>
<th style="width: 100px;">Level</th>
<th style="width: 120px;">XP</th>
<th style="width: 150px;">Joined</th>
</tr>
</thead>
<tbody>
${platformStats.recentUsers.map(user => {
const joinDate = new Date(user.created_at);
const daysAgo = Math.floor((new Date() - joinDate) / (1000 * 60 * 60 * 24));
const timeAgo = daysAgo === 0 ? 'Today' : daysAgo === 1 ? 'Yesterday' : `${daysAgo} days ago`;
return `
<tr>
<td>
<div style="display: flex; align-items: center; gap: 10px;">
<img src="${user.avatar_url || 'https://via.placeholder.com/32'}"
alt="${user.username}"
style="width: 32px; height: 32px; border-radius: 50%;">
<span style="font-weight: 500;">${user.username}</span>
</div>
</td>
<td style="color: ${colors.gray};">${user.email || 'N/A'}</td>
<td style="text-align: center;">
<span class="badge-pill">Level ${user.level || 1}</span>
</td>
<td style="text-align: center; font-weight: 600; color: ${colors.navy};">
${(user.total_xp || 0).toLocaleString()}
</td>
<td style="color: ${colors.gray};">${timeAgo}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- Quick Actions -->
<div class="section-header" style="margin-top: 50px;">
<h3 class="section-title">⚡ Quick Actions</h3>
</div>
<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 40px;">
<a href="/content/admin/users.qmd" style="
padding: 16px 32px;
background: ${colors.teal};
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 0.2s;
display: inline-block;
" onmouseover="this.style.background='#138D75'" onmouseout="this.style.background='${colors.teal}'">
👥 Manage Users
</a>
<a href="/content/admin/content-inventory.qmd" style="
padding: 16px 32px;
background: ${colors.navy};
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 0.2s;
display: inline-block;
" onmouseover="this.style.background='#1A252F'" onmouseout="this.style.background='${colors.navy}'">
📚 Content Inventory
</a>
<a href="/content/admin/badges.qmd" style="
padding: 16px 32px;
background: ${colors.orange};
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 0.2s;
display: inline-block;
" onmouseover="this.style.background='#CA6F1E'" onmouseout="this.style.background='${colors.orange}'">
🏆 Badge Management
</a>
<a href="/content/admin/analytics.qmd" style="
padding: 16px 32px;
background: ${colors.blue};
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 0.2s;
display: inline-block;
" onmouseover="this.style.background='#2874A6'" onmouseout="this.style.background='${colors.blue}'">
📈 Analytics
</a>
</div>
` : ''}`
Admin Resources
For detailed information on managing the platform, see:
- User Management: View all users, search by name/email, filter by level
- Content Inventory: Track games, labs, simulations, knowledge checks across chapters
- Badge Management: View badge distribution, manually award badges
- Analytics: Deep dive into platform metrics with charts and visualizations
Contact the primary admin (ngcharithperera@gmail.com) for questions or access requests.