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 || '🏅'
};
});
}