๐ Your Learning Dashboard
Welcome to your personalized learning dashboard! Track your progress, view earned badges, and see your learning statistics.
<div style="font-size: 3em; margin-bottom: 20px;">โณ</div>
<p style="color: #7F8C8D;">Loading your dashboard...</p>
Show code
colors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
darkGray: "#34495E",
green: "#27AE60",
red: "#E74C3C",
purple: "#9B59B6",
blue: "#3498DB",
white: "#FFFFFF"
})
// Check authentication and load user data
userData = {
const user = Auth.getCurrentUser();
if (!user) {
document.getElementById('auth-check').innerHTML = `
<div style="text-align: center; padding: 60px 20px; background: linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkGray} 100%); border-radius: 12px; color: white;">
<div style="font-size: 3em; margin-bottom: 20px;">๐</div>
<h2 style="margin: 0 0 10px 0;">Sign In Required</h2>
<p style="margin: 0 0 25px 0; opacity: 0.9;">Please sign in to view your dashboard</p>
<button onclick="Auth.login()" style="
padding: 12px 32px;
background: ${colors.teal};
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
">
<i class="fab fa-github"></i> Sign in with GitHub
</button>
</div>
`;
return null;
}
// User is authenticated, load data from Supabase via UnifiedProgress
let totalXP = 0;
let level = 1;
let streak = 0;
let progressData = [];
let badgesData = [];
try {
// Get XP and level from UnifiedProgress (syncs with Supabase)
if (typeof UnifiedProgress !== 'undefined') {
await UnifiedProgress.loadFromCloud();
totalXP = UnifiedProgress.getXP();
level = UnifiedProgress.getLevel();
streak = UnifiedProgress.getStreak();
progressData = await UnifiedProgress.getAllProgress() || [];
}
// Get badges from Supabase API
if (typeof IoTClassAPI !== 'undefined' && IoTClassAPI.Badge) {
badgesData = await IoTClassAPI.Badge.getUserBadges(user.id) || [];
}
} catch (err) {
console.warn('Error loading dashboard data:', err);
}
document.getElementById('auth-check').style.display = 'none';
document.getElementById('dashboard-content').style.display = 'block';
// Count completed chapters
const completedChapters = Array.isArray(progressData)
? progressData.filter(p => p.completed || p.progress_percent === 100)
: [];
return {
user: user,
totalXP: totalXP || 0,
level: level || 1,
streak: streak || 0,
chaptersCompleted: completedChapters.length,
badgesEarned: badgesData.length,
joinDate: user.created_at || new Date().toISOString(),
lastActive: new Date().toISOString(),
progress: completedChapters,
badges: badgesData
};
}
// Profile Header
html`
${userData ? html`
<div style="
background: linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkGray} 100%);
padding: 40px 20px;
border-radius: 12px;
color: white;
margin-bottom: 30px;
">
<div style="
display: flex;
align-items: center;
gap: 30px;
flex-wrap: wrap;
">
<img src="${userData.user.avatar}"
alt="${userData.user.name}"
style="
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid white;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
">
<div style="flex: 1;">
<h1 style="margin: 0 0 10px 0; font-size: 2.5em;">${userData.user.name}</h1>
<p style="margin: 0 0 15px 0; opacity: 0.9; font-size: 1.1em;">@${userData.user.login}</p>
<div style="
display: flex;
gap: 20px;
flex-wrap: wrap;
">
<div>
<div style="font-size: 1.8em; font-weight: bold;">${userData.level}</div>
<div style="opacity: 0.8; font-size: 0.9em;">Level</div>
</div>
<div>
<div style="font-size: 1.8em; font-weight: bold;">${userData.totalXP.toLocaleString()}</div>
<div style="opacity: 0.8; font-size: 0.9em;">Total XP</div>
</div>
<div>
<div style="font-size: 1.8em; font-weight: bold;">${userData.streak || 0}</div>
<div style="opacity: 0.8; font-size: 0.9em;">Day Streak ๐ฅ</div>
</div>
<div>
<div style="font-size: 1.8em; font-weight: bold;">${userData.badgesEarned}</div>
<div style="opacity: 0.8; font-size: 0.9em;">Badges</div>
</div>
</div>
</div>
</div>
</div>
` : ""}
`
Show code
html`
${userData ? html`
<div style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
">
<!-- Chapters Completed -->
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div style="font-size: 3em; margin-bottom: 10px;">๐</div>
<div style="
font-size: 2.5em;
font-weight: bold;
color: ${colors.blue};
margin-bottom: 5px;
">${userData.chaptersCompleted}</div>
<div style="color: ${colors.gray};">Chapters Complete</div>
</div>
<!-- Total XP -->
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div style="font-size: 3em; margin-bottom: 10px;">โญ</div>
<div style="
font-size: 2.5em;
font-weight: bold;
color: ${colors.orange};
margin-bottom: 5px;
">${userData.totalXP.toLocaleString()}</div>
<div style="color: ${colors.gray};">Experience Points</div>
</div>
<!-- Level -->
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div style="font-size: 3em; margin-bottom: 10px;">๐ฏ</div>
<div style="
font-size: 2.5em;
font-weight: bold;
color: ${colors.teal};
margin-bottom: 5px;
">${userData.level}</div>
<div style="color: ${colors.gray};">Current Level</div>
</div>
<!-- Badges -->
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div style="font-size: 3em; margin-bottom: 10px;">๐</div>
<div style="
font-size: 2.5em;
font-weight: bold;
color: ${colors.purple};
margin-bottom: 5px;
">${userData.badgesEarned}</div>
<div style="color: ${colors.gray};">Badges Earned</div>
</div>
<!-- Streak -->
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div style="font-size: 3em; margin-bottom: 10px;">๐ฅ</div>
<div style="
font-size: 2.5em;
font-weight: bold;
color: ${colors.red};
margin-bottom: 5px;
">${userData.streak || 0}</div>
<div style="color: ${colors.gray};">Day Streak</div>
</div>
</div>
` : ""}
`
Show code
html`
${userData ? html`
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
">
<h2 style="margin: 0 0 20px 0; color: ${colors.navy};">๐ Level Progress</h2>
${(() => {
const currentLevelXP = (userData.level - 1) * (userData.level - 1) * 100;
const nextLevelXP = userData.level * userData.level * 100;
const xpProgress = userData.totalXP - currentLevelXP;
const xpNeeded = nextLevelXP - currentLevelXP;
const progressPercent = Math.min((xpProgress / xpNeeded) * 100, 100);
return html`
<div style="margin-bottom: 15px;">
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
">
<span style="font-weight: 500; font-size: 1.1em;">
Level ${userData.level} โ ${userData.level + 1}
</span>
<span style="color: ${colors.gray};">
${xpProgress.toLocaleString()} / ${xpNeeded.toLocaleString()} XP
</span>
</div>
<div style="
width: 100%;
height: 24px;
background: ${colors.lightGray};
border-radius: 12px;
overflow: hidden;
position: relative;
">
<div style="
width: ${progressPercent}%;
height: 100%;
background: linear-gradient(90deg, ${colors.teal} 0%, #1ABC9C 100%);
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 10px;
">
<span style="
color: white;
font-weight: bold;
font-size: 0.85em;
">${Math.round(progressPercent)}%</span>
</div>
</div>
</div>
<p style="color: ${colors.gray}; margin: 0;">
You need ${(xpNeeded - xpProgress).toLocaleString()} more XP to reach Level ${userData.level + 1}
</p>
`;
})()}
</div>
` : ""}
`
Show code
html`
${userData && userData.chaptersCompleted > 0 ? html`
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
">
<h2 style="margin: 0 0 20px 0; color: ${colors.navy};">๐ Recent Progress</h2>
<div style="display: flex; flex-direction: column; gap: 15px;">
${userData.progress.slice(0, 5).map(p => {
// Handle both old format (chapterPath) and new Supabase format (content_id)
const contentPath = p.content_id || p.chapterPath || 'Unknown';
const completedDate = p.completed_at || p.completedDate || new Date().toISOString();
const displayName = contentPath.split('/').pop().replace('.html', '').replace('.qmd', '').replace(/-/g, ' ');
return html`
<div style="
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: ${colors.lightGray};
border-radius: 8px;
">
<div style="
font-size: 2em;
flex-shrink: 0;
">โ
</div>
<div style="flex: 1;">
<div style="font-weight: 500; color: ${colors.navy}; text-transform: capitalize;">
${displayName}
</div>
<div style="font-size: 0.9em; color: ${colors.gray};">
Completed ${new Date(completedDate).toLocaleDateString()}
</div>
</div>
<div style="
background: ${colors.teal};
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: bold;
">+100 XP</div>
</div>
`})}
</div>
</div>
` : ""}
`
Show code
html`
${userData && userData.badgesEarned > 0 ? html`
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
">
<h2 style="margin: 0 0 20px 0; color: ${colors.navy};">๐ Your Badges</h2>
<div style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
">
${userData.badges.map(badge => {
// Handle both old format and new Supabase format
const badgeName = badge.badge_name || badge.badgeName || badge.badge_id || 'Badge';
const earnedDate = badge.earned_at || badge.earnedDate || new Date().toISOString();
const badgeIcon = badge.icon_url ? html`<img src="${badge.icon_url}" alt="${badgeName}" style="width: 48px; height: 48px;">` : html`<span style="font-size: 3em;">๐</span>`;
return html`
<div style="
background: linear-gradient(135deg, ${colors.lightGray} 0%, #E8EAED 100%);
padding: 20px;
border-radius: 12px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="margin-bottom: 10px;">${badgeIcon}</div>
<div style="
font-weight: 500;
color: ${colors.navy};
margin-bottom: 5px;
font-size: 0.95em;
">${badgeName}</div>
<div style="
font-size: 0.8em;
color: ${colors.gray};
">Earned ${new Date(earnedDate).toLocaleDateString()}</div>
</div>
`})}
</div>
</div>
` : ""}
`
Show code
html`
${userData ? html`
<div style="
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h2 style="margin: 0 0 20px 0; color: ${colors.navy};">๐ Quick Actions</h2>
<div style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
">
<a href="/apps/content-hub/index.html#games" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.teal} 0%, #138D75 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐ฎ</div>
<div style="font-weight: 500;">Play Games</div>
</a>
<a href="/apps/content-hub/index.html#simulations" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkGray} 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐ฌ</div>
<div style="font-weight: 500;">Run Simulations</div>
</a>
<a href="/apps/content-hub/index.html#labs" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.orange} 0%, #CA6F1E 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐งช</div>
<div style="font-weight: 500;">Try Labs</div>
</a>
<a href="/leaderboard.html" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.purple} 0%, #7D3C98 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐</div>
<div style="font-weight: 500;">Leaderboard</div>
</a>
<a href="/content/hubs/knowledge-gaps.html" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.blue} 0%, #2980B9 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐</div>
<div style="font-weight: 500;">Knowledge Gaps</div>
</a>
<a href="/content/hubs/learning-paths.html" style="
display: block;
padding: 20px;
background: linear-gradient(135deg, ${colors.green} 0%, #1E8449 100%);
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
transition: transform 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)'"
onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 2.5em; margin-bottom: 10px;">๐ค๏ธ</div>
<div style="font-weight: 500;">Learning Paths</div>
</a>
</div>
</div>
` : ""}
`
- Complete chapters: +100 XP each
- Answer knowledge checks: +10 XP per correct answer
- Complete Wokwi labs: +50 XP each
- Score 90%+ on games: +25 XP
- Complete capstone projects: +500 XP
- 7-day learning streak: +100 XP bonus