Track all educational content across the IoT Class platform.
Show code
Auth =newPromise((resolve, reject) => {const authIsReady = () =>window.__authReady&&typeofwindow.Auth?.getCurrentUser==='function';if (authIsReady()) {resolve(window.Auth);return; }const timeoutMs =10000;const timer =setTimeout(() => {window.removeEventListener('auth:ready', onAuthReady);reject(newError('Sign-in system failed to load. Please refresh the page.')); }, timeoutMs);functiononAuthReady() {clearTimeout(timer);window.removeEventListener('auth:ready', onAuthReady);if (authIsReady()) {resolve(window.Auth); } else {reject(newError('Sign-in system failed to load. Please refresh the page.')); } }window.addEventListener('auth:ready', onAuthReady);})// Colorscolors = ({navy:'#2C3E50',teal:'#16A085',orange:'#E67E22',gray:'#7F8C8D'})// Check authentication and admin statuscurrentUser = {const user = Auth.getCurrentUser();if (!user) returnnull;// B8: the admin role is a database fact -- public.is_admin(), backed by the// user_roles table and evaluated by the same RLS policies that guard every// query below. Auth.isUserAdmin() only string-matches the adminUsers list// that ships in public JavaScript, so it is not a control.const isAdmin =window.IoTClassSupabase?.isAdminServerSide?awaitwindow.IoTClassSupabase.isAdminServerSide():false;return { ...user, isAdmin };}// Load content inventory from gallery indicescontentInventory = {if (!currentUser ||!currentUser.isAdmin) {returnnull; }try {// Load all gallery indicesconst [gamesIndex, simsIndex, labsIndex, checksIndex, squadIndex] =awaitPromise.all([fetch('/assets/data/games-master-index.json').then(r => r.json()).catch(() => ({ galleries: {} })),fetch('/assets/data/simulations-master-index.json').then(r => r.json()).catch(() => ({ galleries: {} })),fetch('/assets/data/labs-master-index.json').then(r => r.json()).catch(() => ({ galleries: {} })),fetch('/assets/data/knowledge-checks-master-index.json').then(r => r.json()).catch(() => ({ galleries: {} })),fetch('/assets/data/sensor-squad-master-index.json').then(r => r.json()).catch(() => ({ galleries: {} })) ]);// Aggregate by chapterconst chapterMap =newMap();// Helper to add content to chapterconst addToChapter = (chapterPath, type) => {if (!chapterMap.has(chapterPath)) { chapterMap.set(chapterPath, {chapter: chapterPath,games:0,simulations:0,labs:0,knowledgeChecks:0,sensorSquad:0 }); }const entry = chapterMap.get(chapterPath); entry[type]++; };// Process gamesObject.values(gamesIndex.galleries|| {}).forEach(gallery => { (gallery.items|| []).forEach(item => {if (item.chapter_path) {addToChapter(item.chapter_path,'games'); } }); });// Process simulationsObject.values(simsIndex.galleries|| {}).forEach(gallery => { (gallery.items|| []).forEach(item => {if (item.chapter_path) {addToChapter(item.chapter_path,'simulations'); } }); });// Process labsObject.values(labsIndex.galleries|| {}).forEach(gallery => { (gallery.items|| []).forEach(item => {if (item.chapter_path) {addToChapter(item.chapter_path,'labs'); } }); });// Process knowledge checksObject.values(checksIndex.galleries|| {}).forEach(gallery => { (gallery.items|| []).forEach(item => {if (item.chapter_path) {addToChapter(item.chapter_path,'knowledgeChecks'); } }); });// Process Sensor SquadObject.values(squadIndex.galleries|| {}).forEach(gallery => { (gallery.items|| []).forEach(item => {if (item.chapter_path) {addToChapter(item.chapter_path,'sensorSquad'); } }); });// Convert to array and sort by chapter nameconst inventory =Array.from(chapterMap.values()).sort((a, b) => a.chapter.localeCompare(b.chapter));// Calculate totalsconst totals = inventory.reduce((acc, item) => ({games: acc.games+ item.games,simulations: acc.simulations+ item.simulations,labs: acc.labs+ item.labs,knowledgeChecks: acc.knowledgeChecks+ item.knowledgeChecks,sensorSquad: acc.sensorSquad+ item.sensorSquad }), { games:0,simulations:0,labs:0,knowledgeChecks:0,sensorSquad:0 });return { inventory, totals,chaptersWithContent: inventory.length,chaptersWithGaps: inventory.filter(item => item.games===0|| item.labs===0|| item.knowledgeChecks===0 ).length }; } catch (error) {console.error('Error loading content inventory:', error);returnnull; }}// Filter stateviewState = ({ search:'',sortBy:'chapter',filterType:'all' })// Filtered and sorted inventoryfilteredInventory = {if (!contentInventory) return [];let filtered = contentInventory.inventory;// Apply search filterif (viewState.search) {const searchLower = viewState.search.toLowerCase(); filtered = filtered.filter(item => item.chapter.toLowerCase().includes(searchLower) ); }// Apply type filterif (viewState.filterType!=='all') { filtered = filtered.filter(item => {switch (viewState.filterType) {case'with-gaps':return item.games===0|| item.labs===0|| item.knowledgeChecks===0;case'complete':return item.games>0&& item.labs>0&& item.knowledgeChecks>0;case'games-only':return item.games>0;case'labs-only':return item.labs>0;case'checks-only':return item.knowledgeChecks>0;default:returntrue; } }); }// Apply sorting filtered.sort((a, b) => {switch (viewState.sortBy) {case'chapter':return a.chapter.localeCompare(b.chapter);case'games':return b.games- a.games;case'simulations':return b.simulations- a.simulations;case'labs':return b.labs- a.labs;case'checks':return b.knowledgeChecks- a.knowledgeChecks;case'total':return (b.games+ b.simulations+ b.labs+ b.knowledgeChecks) - (a.games+ a.simulations+ a.labs+ a.knowledgeChecks);default:return0; } });return filtered;}
Show code
// Render access denied if not adminhtml`${!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}; margin-bottom: 24px;"> You must be an administrator to access this page. </p> </div>`:''}`