👥 User Management
View and manage all registered users on the IoT Class platform.
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 all users via Supabase
allUsers = {
if (! currentUser || ! currentUser. isAdmin ) {
return null ;
}
try {
const users = await IoTClassAPI. Admin . getAllUsers ();
// Admin list from config
const adminUsernames = window . IoTClassSupabase ?. CONFIG ?. adminUsers || ['ngcharithperera' ];
// Enhance user data for display
const enhancedUsers = users. map (user => ({
... user,
isAdmin : adminUsernames. includes (user. username ),
xpProgress : ((user. total_xp || 0 ) % 100 ) / 100 , // Progress to next level
// Map Supabase field names for compatibility
totalXP : user. total_xp ,
badgesEarned : user. badges_count || 0 ,
lastActive : user. updated_at ,
userId : user. id
}));
return enhancedUsers;
} catch (error) {
console . error ('Error loading users:' , error);
return null ;
}
}
// Calculate user statistics
userStats = {
if (! allUsers) return null ;
const total = allUsers. length ;
const sevenDaysAgo = new Date ();
sevenDaysAgo. setDate (sevenDaysAgo. getDate () - 7 );
const activeUsers = allUsers. filter (u => {
if (! u. updated_at ) return false ;
return new Date (u. updated_at ) >= sevenDaysAgo;
}). length ;
const avgXP = total > 0 ? Math . round (allUsers. reduce ((sum, u) => sum + (u. total_xp || 0 ), 0 ) / total) : 0 ;
const avgLevel = total > 0 ? (allUsers. reduce ((sum, u) => sum + (u. level || 1 ), 0 ) / total). toFixed (1 ) : 0 ;
const totalBadges = allUsers. reduce ((sum, u) => sum + (u. badgesEarned || 0 ), 0 );
const usersWithBadges = allUsers. filter (u => (u. badgesEarned || 0 ) > 0 ). length ;
return {
total,
active : activeUsers,
avgXP,
avgLevel,
totalBadges,
usersWithBadges
};
}
// Filter state - use Supabase field names
viewState = ({ search : '' , sortBy : 'total_xp' , sortDir : 'desc' , filterLevel : 'all' , filterStatus : 'all' })
// Filtered and sorted users
filteredUsers = {
if (! allUsers) return [];
let filtered = [... allUsers];
// Apply search filter
if (viewState. search ) {
const searchLower = viewState. search . toLowerCase ();
filtered = filtered. filter (u =>
(u. username || '' ). toLowerCase (). includes (searchLower) ||
(u. email || '' ). toLowerCase (). includes (searchLower)
);
}
// Apply level filter
if (viewState. filterLevel !== 'all' ) {
const minLevel = parseInt (viewState. filterLevel );
const maxLevel = minLevel + 9 ; // 1-10, 11-20, etc.
filtered = filtered. filter (u => {
const level = u. level || 1 ;
return level >= minLevel && level <= maxLevel;
});
}
// Apply status filter
if (viewState. filterStatus !== 'all' ) {
const sevenDaysAgo = new Date ();
sevenDaysAgo. setDate (sevenDaysAgo. getDate () - 7 );
if (viewState. filterStatus === 'active' ) {
filtered = filtered. filter (u => u. updated_at && new Date (u. updated_at ) >= sevenDaysAgo);
} else if (viewState. filterStatus === 'inactive' ) {
filtered = filtered. filter (u => ! u. updated_at || new Date (u. updated_at ) < sevenDaysAgo);
} else if (viewState. filterStatus === 'admin' ) {
filtered = filtered. filter (u => u. isAdmin );
}
}
// Apply sorting - map legacy field names to Supabase field names
const sortFieldMap = {
'totalXP' : 'total_xp' ,
'joinDate' : 'created_at' ,
'lastActive' : 'updated_at'
};
const sortField = sortFieldMap[viewState. sortBy ] || viewState. sortBy ;
filtered. sort ((a, b) => {
let aVal = a[sortField] || 0 ;
let bVal = b[sortField] || 0 ;
// Handle string fields
if (typeof aVal === 'string' ) {
aVal = aVal. toLowerCase ();
bVal = (bVal || '' ). toLowerCase ();
return viewState. sortDir === 'asc' ? aVal. localeCompare (bVal) : bVal. localeCompare (aVal);
}
// Handle numeric fields
return viewState. sortDir === 'asc' ? aVal - bVal : bVal - aVal;
});
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 user management if admin
html ` ${ currentUser?. isAdmin && allUsers && userStats ? `
<!-- Statistics Row -->
<div class="stats-row">
<div class="stat-box">
<div class="stat-value"> ${ userStats. total } </div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-box">
<div class="stat-value" style="color: ${ colors. teal } ;"> ${ userStats. active } </div>
<div class="stat-label">Active (7 days)</div>
</div>
<div class="stat-box">
<div class="stat-value"> ${ userStats. avgXP . toLocaleString ()} </div>
<div class="stat-label">Avg XP</div>
</div>
<div class="stat-box">
<div class="stat-value"> ${ userStats. avgLevel } </div>
<div class="stat-label">Avg Level</div>
</div>
<div class="stat-box">
<div class="stat-value"> ${ userStats. totalBadges } </div>
<div class="stat-label">Total Badges</div>
</div>
<div class="stat-box">
<div class="stat-value"> ${ userStats. usersWithBadges } </div>
<div class="stat-label">Users with Badges</div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<input
type="text"
class="filter-input"
placeholder="Search by name or email..."
value=" ${ viewState. search } "
oninput="viewState = { ...viewState, search: this.value }"
/>
<select
class="filter-select"
value=" ${ viewState. filterLevel } "
onchange="viewState = { ...viewState, filterLevel: this.value }"
>
<option value="all">All Levels</option>
<option value="1">Level 1-10</option>
<option value="11">Level 11-20</option>
<option value="21">Level 21-30</option>
<option value="31">Level 31+</option>
</select>
<select
class="filter-select"
value=" ${ viewState. filterStatus } "
onchange="viewState = { ...viewState, filterStatus: this.value }"
>
<option value="all">All Users</option>
<option value="active">Active (7 days)</option>
<option value="inactive">Inactive</option>
<option value="admin">Admins Only</option>
</select>
<select
class="filter-select"
value=" ${ viewState. sortBy } "
onchange="viewState = { ...viewState, sortBy: this.value }"
>
<option value="total_xp">Sort by XP</option>
<option value="level">Sort by Level</option>
<option value="badgesEarned">Sort by Badges</option>
<option value="username">Sort by Name</option>
<option value="created_at">Sort by Join Date</option>
<option value="updated_at">Sort by Last Active</option>
</select>
<button
class="filter-select"
onclick="viewState = { ...viewState, sortDir: viewState.sortDir === 'desc' ? 'asc' : 'desc' }"
style="cursor: pointer; background: white;"
>
${ viewState. sortDir === 'desc' ? '↓ Desc' : '↑ Asc' }
</button>
</div>
<!-- Users Table -->
<table class="users-table">
<thead>
<tr>
<th onclick="viewState = { ...viewState, sortBy: 'username' }">User</th>
<th onclick="viewState = { ...viewState, sortBy: 'email' }">Email</th>
<th style="width: 100px; text-align: center;" onclick="viewState = { ...viewState, sortBy: 'level' }">Level</th>
<th style="width: 120px; text-align: center;" onclick="viewState = { ...viewState, sortBy: 'totalXP' }">XP</th>
<th style="width: 100px; text-align: center;" onclick="viewState = { ...viewState, sortBy: 'badgesEarned' }">Badges</th>
<th style="width: 150px;" onclick="viewState = { ...viewState, sortBy: 'lastActive' }">Last Active</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
${ filteredUsers. length === 0 ? `
<tr>
<td colspan="7" style="text-align: center; padding: 40px; color: ${ colors. gray } ;">
No users match the current filter.
</td>
</tr>
` : filteredUsers. map (user => {
const lastActive = user. updated_at ? new Date (user. updated_at ) : null ;
const daysAgo = lastActive ? Math . floor ((new Date () - lastActive) / (1000 * 60 * 60 * 24 )) : null ;
const activeText = daysAgo === null ? 'Never' :
daysAgo === 0 ? 'Today' :
daysAgo === 1 ? 'Yesterday' :
` ${ daysAgo} d ago` ;
return `
<tr>
<td>
<div style="display: flex; align-items: center;">
<img src=" ${ user. avatar_url || 'https://via.placeholder.com/40' } "
alt=" ${ user. username } "
class="user-avatar">
<div>
<div style="font-weight: 500;"> ${ user. username } </div>
${ user. isAdmin ? '<span class="admin-badge">ADMIN</span>' : '' }
</div>
</div>
</td>
<td style="color: ${ colors. gray } ;"> ${ user. email || 'N/A' } </td>
<td style="text-align: center;">
<span class="level-badge">Level ${ user. level || 1 } </span>
</td>
<td style="text-align: center;">
<div style="font-weight: 600; color: ${ colors. navy } ; margin-bottom: 4px;">
${ (user. total_xp || 0 ). toLocaleString ()}
</div>
<div class="progress-bar" style="margin: 0 auto;">
<div class="progress-fill" style="width: ${ user. xpProgress * 100 } %;"></div>
</div>
</td>
<td style="text-align: center; font-weight: 600; color: ${ colors. teal } ;">
${ user. badgesEarned || 0 }
</td>
<td style="color: ${ colors. gray } ; font-size: 13px;">
${ activeText}
</td>
<td style="text-align: center;">
<button
class="view-button"
onclick="window.location.href='/content/user/dashboard.qmd?user= ${ user. id } '"
>
View
</button>
</td>
</tr>
` ;
}). join ('' )}
</tbody>
</table>
<div style="text-align: center; color: ${ colors. gray } ; font-size: 14px; margin-top: 20px;">
Showing ${ filteredUsers. length } of ${ allUsers. length } users
</div>
` : '' } `
User Management Tips
Search : Find users by username or email address
Filters : - Level : Filter by user level range (1-10, 11-20, etc.) - Status : View active users (activity in last 7 days), inactive users, or admins only
Sorting : Click column headers to sort by any field. Use the Desc/Asc toggle to reverse sort order.
View Profile : Click “View” to see detailed user profile with progress, badges, and activity history.