supportServices = {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const waitFor = async (getter, timeoutMs = 5000) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const value = getter();
if (value) {
return value;
}
await wait(100);
}
return null;
};
const auth = await waitFor(() => {
const candidate = window.Auth;
return candidate &&
typeof candidate.getCurrentUser === 'function' &&
typeof candidate.isUserAuthenticated === 'function'
? candidate
: null;
});
const api = await waitFor(() => {
const candidate = window.IoTClassAPI;
return candidate && candidate.User && candidate.Badge ? candidate : null;
});
const badgeSystem = await waitFor(() => {
const candidate = window.BadgeSystem;
return candidate && (candidate.badgeCatalog || typeof candidate.init === 'function')
? candidate
: null;
}, 3000);
return {
auth,
api,
badgeSystem,
isAuthenticated: Boolean(auth?.isUserAuthenticated?.()),
};
}
// Colors
colors = ({
navy: '#2C3E50',
teal: '#16A085',
orange: '#E67E22',
gray: '#7F8C8D',
lightGray: '#ECF0F1'
})
normalizeCategoryKey = (value) => {
return String(value || 'uncategorized')
.trim()
.toLowerCase()
.replace(/[_\s]+/g, '-');
}
formatCategoryLabel = (key) => {
return key
.split('-')
.filter(Boolean)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Check authentication
currentUser = {
const auth = supportServices.auth;
if (!auth) {
return null;
}
const user = auth.getCurrentUser();
if (!user) {
return null;
}
// Get user profile from Supabase
const userApi = supportServices.api?.User;
const userData = userApi?.getProfile
? await userApi.getProfile(user.id)
: {};
return {
...user,
...userData
};
}
// Badge catalog (all 15 badges)
badgeCatalog = {
const badgeSystem = supportServices.badgeSystem;
if (badgeSystem?.init) {
await badgeSystem.init();
}
if (Array.isArray(badgeSystem?.badgeCatalog) && badgeSystem.badgeCatalog.length > 0) {
return badgeSystem.badgeCatalog.map(badge => ({
...badge,
emoji: badge.emoji || badge.icon || 'π
',
rarity: badge.rarity || 'common',
xp_reward: badge.xp_reward || 0
}));
}
const badgeApi = supportServices.api?.Badge;
if (badgeApi?.getAllBadgeDefinitions) {
const definitions = await badgeApi.getAllBadgeDefinitions();
if (definitions && definitions.length > 0) {
return definitions.map(badge => ({
...badge,
emoji: badge.emoji || badge.icon || 'π
',
rarity: badge.rarity || 'common',
xp_reward: badge.xp_reward || 0
}));
}
}
return [];
}
// User's earned badges - Using Supabase via BadgeSystem or direct API
userBadges = {
if (!supportServices.isAuthenticated || !currentUser) {
return [];
}
try {
const badgeSystem = supportServices.badgeSystem;
const badgeApi = supportServices.api?.Badge;
// 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
if (!badgeApi?.getUserBadges) {
return [];
}
const badges = await badgeApi.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 [];
}
}
categoryMeta = {
const defaults = {
milestone: { label: 'Milestone', emoji: 'β', accent: '#E67E22' },
achievement: { label: 'Achievement', emoji: 'β
', accent: '#16A085' },
streak: { label: 'Streak', emoji: 'π₯', accent: '#C0392B' },
specialty: { label: 'Specialty', emoji: 'π§', accent: '#2C3E50' },
special: { label: 'Special', emoji: 'π
', accent: '#8E44AD' },
uncategorized: { label: 'Other', emoji: 'π·οΈ', accent: '#7F8C8D' }
};
const meta = {};
const register = (rawCategory) => {
const key = normalizeCategoryKey(rawCategory);
if (!meta[key]) {
const fallback = defaults.uncategorized;
meta[key] = defaults[key] || {
label: formatCategoryLabel(key),
emoji: fallback.emoji,
accent: fallback.accent
};
}
return key;
};
badgeCatalog.forEach(badge => register(badge.category));
userBadges.forEach(badge => {
register(
badge.category ||
badge.badge_category ||
badge.badgeCategory ||
badge.badge_definition?.category ||
badge.badgeDefinition?.category
);
});
if (Object.keys(meta).length === 0) {
register('milestone');
}
return meta;
}
// Badge progress statistics
badgeStats = {
const earnedCount = userBadges.length;
const totalCount = badgeCatalog.length;
const completionPercent = totalCount > 0 ? Math.round((earnedCount / totalCount) * 100) : 0;
const makeBucket = (key) => ({
earned: 0,
total: 0,
label: categoryMeta[key]?.label || formatCategoryLabel(key),
emoji: categoryMeta[key]?.emoji || 'π
',
accent: categoryMeta[key]?.accent || colors.gray
});
const byCategory = {};
Object.keys(categoryMeta).forEach((key) => {
byCategory[key] = makeBucket(key);
});
const resolveBadgeCategory = (badge) => {
return normalizeCategoryKey(
badge.category ||
badge.badge_category ||
badge.badgeCategory ||
badge.badge_definition?.category ||
badge.badgeDefinition?.category
);
};
badgeCatalog.forEach((badge) => {
const key = resolveBadgeCategory(badge);
if (!byCategory[key]) {
byCategory[key] = makeBucket(key);
}
byCategory[key].total++;
});
userBadges.forEach((badge) => {
const key = resolveBadgeCategory(badge);
if (!byCategory[key]) {
byCategory[key] = makeBucket(key);
}
byCategory[key].earned++;
});
// Total XP from badges
const totalBadgeXP = userBadges.reduce((sum, badge) => sum + (badge.xp_reward || 0), 0);
const categoryKeys = Object.keys(byCategory).sort((left, right) => {
return byCategory[right].total - byCategory[left].total || left.localeCompare(right);
});
return {
earnedCount,
totalCount,
completionPercent,
byCategory,
categoryKeys,
totalBadgeXP
};
}
// Filter state
viewState = {
return {
view: 'all', // all, earned, not-earned
categoryFilter: 'all' // all or a normalized category key
};
}
// Filtered badges
filteredBadges = {
const earnedBadgeIds = new Set(
userBadges.map((badge) => badge.badgeId || badge.badge_id || badge.id).filter(Boolean)
);
let badges = badgeCatalog;
// Apply view filter
if (viewState.view === 'earned') {
badges = badges.filter(badge =>
earnedBadgeIds.has(badge.id)
);
} else if (viewState.view === 'not-earned') {
badges = badges.filter(badge =>
!earnedBadgeIds.has(badge.id)
);
}
// Apply category filter
if (viewState.categoryFilter !== 'all') {
badges = badges.filter(badge => normalizeCategoryKey(badge.category) === viewState.categoryFilter);
}
// Merge with earned data
return badges.map(badge => {
const earned = userBadges.find(e => (e.badgeId || e.badge_id || e.id) === badge.id);
const categoryKey = normalizeCategoryKey(badge.category);
const categoryInfo = categoryMeta[categoryKey] || {
label: formatCategoryLabel(categoryKey),
emoji: 'π·οΈ',
accent: colors.gray
};
return {
...badge,
category: categoryKey,
categoryLabel: categoryInfo.label,
categoryEmoji: categoryInfo.emoji,
categoryAccent: categoryInfo.accent,
earned: earnedBadgeIds.has(badge.id),
earnedDate: earned?.earnedDate || earned?.earned_at,
emoji: badge.emoji || badge.icon || 'π
'
};
});
}π Badge Showcase
Earn Digital Badges for Your IoT Learning Achievements
TipPutting Numbers to It
Learner engagement can be monitored with a weighted score:
\[ E = 0.5\,Q + 0.3\,L + 0.2\,B \]
where \(Q\) is quiz completion rate, \(L\) is lab completion rate, and \(B\) is badge progress (all normalized 0 to 1).
Worked example: For \(Q=0.82\), \(L=0.55\), and \(B=0.40\), engagement score is \(E=0.5(0.82)+0.3(0.55)+0.2(0.40)=0.655\). Tracking this weekly helps detect drop-off early.
π 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!
NoteWhat are Digital Badges?
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)
Badge Categories
β Milestone Badges
Track major completion and XP thresholds such as finishing your first chapter, completing large chapter counts, or reaching major point milestones.
β Achievement Badges
Reward specific accomplishments like quiz excellence, hands-on lab completion, or standout performance on a focused activity.
π₯ Streak Badges
Recognize consistent learning habits over time, such as maintaining multi-day study streaks.
π§ Specialty Badges
Highlight depth in a technical area such as protocols, security, or architecture once you complete a targeted body of work.
π Special Badges
Reserve a small set of badges for exceptional recognition that does not fit the normal milestone or activity pathways.
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)
TipHow to Use Your Badges
- 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
| Rarity | Color | Description | Percentage |
|---|---|---|---|
| Common | Gray | Easy to earn | 60-70% |
| Uncommon | Green | Moderate effort | 20-30% |
| Rare | Blue | Significant achievement | 5-10% |
| Legendary | Gold | Exceptional mastery | <1% |