1// Supabase Client for Signal Pilot Education 2(function() { 3 'use strict'; 4 5 // Supabase Configuration - loaded from config.js 6 const SUPABASE_URL = window.SUPABASE_CONFIG?.url || ''; 7 const SUPABASE_ANON_KEY = window.SUPABASE_CONFIG?.anonKey || ''; 8 9 // Check if config.js failed to load 10 if (!window.SUPABASE_CONFIG) { 11 console.error('[Supabase] â config.js failed to load. Authentication will not work.'); 12 console.error('[Supabase] Make sure /assets/config.js exists and is accessible.'); 13 } 14 15 // Initialize Supabase client 16 let supabase = null; 17 18 /** 19 * Initialize Supabase client and load library from CDN 20 * @returns {Promise<Object|null>} Supabase client instance or null if failed 21 */ 22 async function initSupabase() { 23 // Load Supabase library from CDN if not already loaded 24 if (typeof window.supabase === 'undefined') { 25 const script = document.createElement('script'); 26 script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2'; 27 document.head.appendChild(script); 28 29 await new Promise((resolve, reject) => { 30 script.onload = resolve; 31 script.onerror = reject; 32 }); 33 } 34 35 // Check if credentials are configured 36 if (!SUPABASE_URL || !SUPABASE_ANON_KEY || 37 SUPABASE_URL === 'YOUR_SUPABASE_URL' || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE' || 38 SUPABASE_ANON_KEY === 'YOUR_SUPABASE_ANON_KEY' || SUPABASE_ANON_KEY === 'YOUR_SUPABASE_ANON_KEY_HERE') { 39 console.error('[Supabase] â Supabase credentials not configured properly.'); 40 console.error('[Supabase] Please check that /assets/config.js has valid url and anonKey.'); 41 return null; 42 } 43 44 // Create Supabase client 45 const supabaseLib = window.supabase; // Save library reference 46 supabase = supabaseLib.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 47 48 // Expose the client globally for other modules (discussions, etc.) 49 window.supabase = supabase; 50 51 logger.log('[Supabase] Client initialized'); 52 return supabase; 53 } 54 55 // Auth State Management 56 let currentUser = null; 57 let authStateListeners = []; 58 let isReloadingForCloudSync = false; // Prevent multiple simultaneous reloads 59 60 // Subscribe to auth state changes 61 /** 62 * Subscribe to authentication state changes 63 * @param {Function} callback - Function called when auth state changes (user) => void 64 * @returns {Function} Unsubscribe function 65 */ 66 function onAuthStateChange(callback) { 67 authStateListeners.push(callback); 68 69 // Return unsubscribe function 70 return () => { 71 authStateListeners = authStateListeners.filter(cb => cb !== callback); 72 }; 73 } 74 75 // Notify all listeners of auth state change 76 function notifyAuthStateChange(user) { 77 currentUser = user; 78 79 // Publish the signed-in user on window. gamification.js and badges.js both 80 // gate on window.currentUser, and nothing had ever assigned it, so XP and 81 // badges were skipped for every visitor including signed-in ones. 82 window.currentUser = user || null; 83 84 authStateListeners.forEach(callback => callback(user)); 85 86 // Update UI 87 updateAuthUI(user); 88 } 89 90 // Initialize auth listener 91 async function initAuthListener() { 92 if (!supabase) return; 93 94 // Check for existing session FIRST 95 const { data: { session } } = await supabase.auth.getSession(); 96 const hadExistingSession = !!session?.user; 97 98 if (hadExistingSession) { 99 logger.log('[Supabase] ð Found existing session on page load'); 100 // Update UI immediately for existing sessions 101 notifyAuthStateChange(session.user); 102 } 103 104 // Set up auth listener for future changes 105 supabase.auth.onAuthStateChange(async (event, session) => { 106 logger.log('[Supabase] Auth event:', event); 107 108 if (event === 'SIGNED_IN') { 109 notifyAuthStateChange(session?.user || null); 110 111 // Only load from cloud on TRUE new sign-ins (not existing sessions) 112 // hadExistingSession means this SIGNED_IN event is from the listener registration, not a real new sign-in 113 if (!hadExistingSession && !isReloadingForCloudSync) { 114 try { 115 logger.log('[Supabase] ð New sign-in detected - loading cloud progress...'); 116 117 const loadResult = await loadProgressFromCloud(); 118 119 if (loadResult?.data) {
120 // Store current user ID so we know who owns this local data 121 localStorage.setItem('sp_user_id', session.user.id); 122 isReloadingForCloudSync = true; 123 logger.log('[Supabase] â Cloud progress loaded. Reloading page...'); 124 setTimeout(() => window.location.reload(), 500); 125 } else { 126 // Check if local data belongs to a different user (safety check) 127 const storedUserId = localStorage.getItem('sp_user_id'); 128 if (storedUserId && storedUserId !== session.user.id) { 129 // Local data belongs to different user - clear it first 130 logger.log('[Supabase] â ï¸ Local data belongs to different user, clearing...'); 131 const keysToRemove = []; 132 for (let i = 0; i < localStorage.length; i++) { 133 const key = localStorage.key(i); 134 if (key && (key.startsWith('sp_edu_') || key.startsWith('sp_lesson') || 135 key.startsWith('sp_learning') || key.startsWith('sp_notes_') || 136 key === 'sp_total_xp' || key === 'sp_xp_log' || 137 key === 'sp_bookmarks' || key === 'sp_favorites' || 138 key === 'sp_downloads' || key === 'sp_activity')) { 139 keysToRemove.push(key); 140 } 141 } 142 keysToRemove.forEach(key => localStorage.removeItem(key)); 143 logger.log('[Supabase] Cleared', keysToRemove.length, 'keys from previous user'); 144 } 145 146 // Store current user ID 147 localStorage.setItem('sp_user_id', session.user.id); 148 149 logger.log('[Supabase] â¹ï¸ No cloud progress found. Syncing local progress to cloud...'); 150 await syncProgressToCloud(); 151 } 152 } catch (error) { 153 console.error('[Supabase] Error during sign-in cloud sync:', error); 154 } 155 } else if (hadExistingSession) { 156 logger.log('[Supabase] â¹ï¸ Skipping cloud reload - existing session, not new sign-in'); 157 } 158 } else if (event === 'USER_UPDATED') { 159 notifyAuthStateChange(session?.user || null); 160 } else if (event === 'SIGNED_OUT') { 161 notifyAuthStateChange(null); 162 } 163 }); 164 } 165 166 // Sign Up 167 /** 168 * Sign up a new user with email and password 169 * @param {string} email - User's email address 170 * @param {string} password - User's password 171 * @param {string} userName - Display name for the user 172 * @returns {Promise<{success: boolean, data?: Object, error?: string}>} Sign up result 173 */ 174 async function signUp(email, password, userName) { 175 if (!supabase) { 176 logger.log('[Supabase] Not initialized, initializing now...'); 177 await initSupabase(); 178 179 if (!supabase) { 180 return { success: false, error: 'Failed to initialize authentication. Please refresh the page and try again.' }; 181 } 182 } 183 184 try { 185 const { data, error } = await supabase.auth.signUp({ 186 email, 187 password, 188 options: { 189 data: { 190 user_name: userName 191 } 192 } 193 }); 194 195 if (error) throw error; 196 197 // Track signup 198 if (typeof trackAchievement === 'function') { 199 trackAchievement('account_created'); 200 } 201 202 if (typeof plausible === 'function') { 203 plausible('User Signed Up'); 204 } 205 206 return { success: true, data }; 207 } catch (error) { 208 console.error('[Supabase] Sign up error:', error); 209 return { success: false, error: error.message }; 210 } 211 } 212 213 // Sign In 214 /** 215 * Sign in an existing user with email and password 216 * @param {string} email - User's email address 217 * @param {string} password - User's password 218 * @returns {Promise<{success: boolean, data?: Object, error?: string}>} Sign in result 219 */ 220 async function signIn(email, password) { 221 if (!supabase) { 222 logger.log('[Supabase] Not initialized, initializing now...'); 223 await initSupabase(); 224 225 if (!supabase) { 226 return { success: false, error: 'Failed to initialize authentication. Please refresh the page and try again.' }; 227 } 228 } 229 230 try { 231 const { data, error } = await supabase.auth.signInWithPassword({ 232 email, 233 password 234 }); 235 236 if (error) throw error; 237 238 if (typeof plausible === 'function') { 239 plausible('User Signed In'); 240 } 241 242 return { success: true, data }; 243 } catch (error) { 244 console.error('[Supabase] Sign in error:', error); 245 return { success: false, error: error.message }; 246 } 247 } 248 249 // Sign Out 250 /** 251 * Sign out the current user 252 * @returns {Promise<{success: boolean, error?: string}>} Sign out result 253 */ 254 async function signOut() { 255 if (!supabase) return; 256 257 try { 258 const { error } = await supabase.auth.signOut(); 259 if (error) throw error; 260 261 // Clear all user progress from localStorage to prevent data leaking to next user 262 logger.log('[Supabase] Clearing local progress data...');
263 const keysToRemove = []; 264 for (let i = 0; i < localStorage.length; i++) { 265 const key = localStorage.key(i); 266 if (key && (key.startsWith('sp_edu_') || key.startsWith('sp_lesson') || 267 key.startsWith('sp_learning') || key.startsWith('sp_notes_') || 268 key === 'sp_total_xp' || key === 'sp_xp_log' || 269 key === 'sp_bookmarks' || key === 'sp_favorites' || 270 key === 'sp_downloads' || key === 'sp_activity' || 271 key === 'sp_last_cloud_sync' || key === 'sp_activity_migrated' || 272 key === 'sp_user_id')) { 273 keysToRemove.push(key); 274 } 275 } 276 keysToRemove.forEach(key => localStorage.removeItem(key)); 277 logger.log('[Supabase] Cleared', keysToRemove.length, 'progress keys from localStorage'); 278 279 // Clear UI and reload to show signed-out state 280 logger.log('[Supabase] Signed out successfully, reloading...'); 281 setTimeout(() => { 282 window.location.reload(); 283 }, 500); 284 285 return { success: true }; 286 } catch (error) { 287 console.error('[Supabase] Sign out error:', error); 288 return { success: false, error: error.message }; 289 } 290 } 291 292 // Get Current User 293 /** 294 * Get the currently authenticated user 295 * @returns {Object|null} Current user object or null if not authenticated 296 */ 297 function getCurrentUser() { 298 return currentUser; 299 } 300 301 // Password Reset 302 /** 303 * Send password reset email to user 304 * @param {string} email - User's email address 305 * @returns {Promise<{success: boolean, error?: string}>} Reset result 306 */ 307 async function resetPassword(email) { 308 if (!supabase) { 309 logger.log('[Supabase] Not initialized, initializing now...'); 310 await initSupabase(); 311 312 if (!supabase) { 313 return { success: false, error: 'Failed to initialize authentication. Please refresh the page and try again.' }; 314 } 315 } 316 317 try { 318 const { error } = await supabase.auth.resetPasswordForEmail(email, { 319 redirectTo: `${window.location.origin}/reset-password.html` 320 }); 321 322 if (error) throw error; 323 324 return { success: true }; 325 } catch (error) { 326 console.error('[Supabase] Password reset error:', error); 327 return { success: false, error: error.message }; 328 } 329 } 330 331 // ========== PROGRESS SYNC ========== 332 333 // Sync progress to cloud 334 /** 335 * Sync local progress data to cloud (Supabase) 336 * Syncs completed lessons, streak data, and notes 337 * @returns {Promise<{success: boolean, data?: Object, error?: string}>} Sync result 338 */ 339 async function syncProgressToCloud() { 340 if (!supabase || !currentUser) { 341 logger.log('[Supabase] Cannot sync: not authenticated'); 342 return { success: false, error: 'Not authenticated' }; 343 } 344 345 try { 346 // Collect ALL progress from localStorage 347 const progress = {}; 348 349 // Get all sp_edu_* keys (lesson completion, achievements, etc.) 350 for (let i = 0; i < localStorage.length; i++) { 351 const key = localStorage.key(i); 352 if (key && (key.startsWith('sp_edu_') || key.startsWith('sp_learning') || key.startsWith('sp_lesson') || key === 'sp_total_xp' || key === 'sp_xp_log')) { 353 progress[key] = localStorage.getItem(key); 354 } 355 } 356 357 // Collect all notes (sp_notes_* format) 358 const notes = {}; 359 for (let i = 0; i < localStorage.length; i++) { 360 const key = localStorage.key(i); 361 if (key && key.startsWith('sp_notes_')) { 362 try { 363 notes[key] = JSON.parse(localStorage.getItem(key)); 364 } catch (e) { 365 console.error('[Supabase] Error parsing note:', key, e); 366 } 367 } 368 } 369 370 const streak = JSON.parse(localStorage.getItem('sp_learning_streak') || '{"current": 0, "best": 0}'); 371 const bookmarks = JSON.parse(localStorage.getItem('sp_bookmarks') || '[]'); 372 const favorites = JSON.parse(localStorage.getItem('sp_favorites') || '[]'); 373 const downloads = JSON.parse(localStorage.getItem('sp_downloads') || '[]'); 374 const activity = JSON.parse(localStorage.getItem('sp_activity') || '{}'); 375 376 logger.log('[Supabase] ð¤ Syncing progress to cloud...', { 377 progressKeys: Object.keys(progress).length, 378 completedLessons: Object.keys(progress).filter(k => k.includes('_completed')).length, 379 totalXP: progress['sp_total_xp'] || 0, 380 streakCurrent: streak.current, 381 notesCount: Object.keys(notes).length, 382 bookmarksCount: bookmarks.length, 383 favoritesCount: favorites.length, 384 downloadsCount: downloads.length, 385 activityDays: Object.keys(activity).length 386 }); 387 388 // Upsert to database 389 const { data, error } = await supabase 390 .from('user_progress') 391 .upsert({ 392 user_id: currentUser.id, 393 progress: progress, 394 streak: streak, 395 notes: notes, 396 bookmarks: bookmarks, 397 favorites: favorites, 398 downloads: downloads, 399 activity: activity,
400 last_synced: new Date().toISOString() 401 }, { 402 onConflict: 'user_id' 403 }) 404 .select(); 405 406 if (error) { 407 console.error('[Supabase] â Sync FAILED:', error); 408 409 // Check if table doesn't exist 410 if (error.code === '42P01' || error.message.includes('relation') || error.message.includes('does not exist')) { 411 console.error('[Supabase] â DATABASE TABLE NOT FOUND!'); 412 console.error('[Supabase] â ï¸ Please run database-setup.sql in your Supabase SQL Editor'); 413 console.error('[Supabase] ð See CLOUD_SYNC_SETUP.md for instructions'); 414 showSyncError('Database not set up. Please run database-setup.sql in Supabase.'); 415 } else { 416 showSyncError(`Sync failed: ${error.message}`); 417 } 418 419 throw error; 420 } 421 422 logger.log('[Supabase] â Progress synced to cloud successfully!', data); 423 showSyncSuccess(); 424 425 // Update last sync time 426 localStorage.setItem('sp_last_cloud_sync', Date.now()); 427 428 return { success: true, data }; 429 } catch (error) { 430 console.error('[Supabase] Sync error:', error); 431 return { success: false, error: error.message }; 432 } 433 } 434 435 // Load progress from cloud 436 /** 437 * Load progress data from cloud to local storage 438 * Restores completed lessons, streak data, and notes 439 * @returns {Promise<{success: boolean, data?: Object, error?: string}>} Load result 440 */ 441 async function loadProgressFromCloud() { 442 if (!supabase || !currentUser) { 443 logger.log('[Supabase] Cannot load: not authenticated'); 444 return { success: false, error: 'Not authenticated' }; 445 } 446 447 try { 448 logger.log('[Supabase] ð¥ Loading progress from cloud...'); 449 450 const { data, error } = await supabase 451 .from('user_progress') 452 .select('*') 453 .eq('user_id', currentUser.id) 454 .single(); 455 456 if (error) { 457 // No data found is ok (new user) 458 if (error.code === 'PGRST116') { 459 logger.log('[Supabase] â¹ï¸ No cloud progress found (new user)'); 460 return { success: true, data: null }; 461 } 462 463 // Check if table doesn't exist 464 if (error.code === '42P01' || error.message.includes('relation') || error.message.includes('does not exist')) { 465 console.error('[Supabase] â DATABASE TABLE NOT FOUND!'); 466 console.error('[Supabase] â ï¸ Please run database-setup.sql in your Supabase SQL Editor'); 467 console.error('[Supabase] ð See CLOUD_SYNC_SETUP.md for instructions'); 468 showSyncError('Database not set up. Please run database-setup.sql in Supabase.'); 469 } 470 471 throw error; 472 } 473 474 if (data) { 475 const completedLessons = Object.keys(data.progress || {}).filter(k => k.includes('_completed')).length; 476 477 logger.log('[Supabase] ð¥ Found cloud progress:', { 478 progressKeys: Object.keys(data.progress || {}).length, 479 completedLessons: completedLessons, 480 streakCurrent: data.streak?.current, 481 notesCount: Object.keys(data.notes || {}).length, 482 bookmarksCount: (data.bookmarks || []).length, 483 favoritesCount: (data.favorites || []).length, 484 downloadsCount: (data.downloads || []).length, 485 activityDays: Object.keys(data.activity || {}).length, 486 lastSynced: data.last_synced 487 }); 488 489 // Restore ALL individual progress keys to localStorage 490 const progress = data.progress || {}; 491 for (const key in progress) { 492 localStorage.setItem(key, progress[key]); 493 } 494 495 // Restore streak 496 localStorage.setItem('sp_learning_streak', JSON.stringify(data.streak || {})); 497 498 // Restore notes (sp_notes_* format) 499 const notes = data.notes || {}; 500 for (const key in notes) { 501 localStorage.setItem(key, JSON.stringify(notes[key])); 502 } 503 504 // Restore bookmarks, favorites, and downloads (MERGE, don't overwrite) 505 // This prevents data loss when cloud is empty but local has data 506 const mergeArrays = (localKey, cloudData) => { 507 const local = JSON.parse(localStorage.getItem(localKey) || '[]'); 508 const cloud = cloudData || []; 509 // Merge by creating a map of unique items by id or url 510 const merged = new Map(); 511 local.forEach(item => {
512 const key = item.id || item.url || JSON.stringify(item); 513 merged.set(key, item); 514 }); 515 cloud.forEach(item => { 516 const key = item.id || item.url || JSON.stringify(item); 517 merged.set(key, item); // Cloud overwrites local for same item 518 }); 519 return Array.from(merged.values()); 520 }; 521 522 const mergedBookmarks = mergeArrays('sp_bookmarks', data.bookmarks); 523 const mergedFavorites = mergeArrays('sp_favorites', data.favorites); 524 const mergedDownloads = mergeArrays('sp_downloads', data.downloads); 525 526 localStorage.setItem('sp_bookmarks', JSON.stringify(mergedBookmarks)); 527 localStorage.setItem('sp_favorites', JSON.stringify(mergedFavorites)); 528 localStorage.setItem('sp_downloads', JSON.stringify(mergedDownloads)); 529 530 logger.log('[Supabase] Merged data - Bookmarks:', mergedBookmarks.length, 'Favorites:', mergedFavorites.length); 531 532 // Restore activity if it exists in cloud 533 if (data.activity && Object.keys(data.activity).length > 0) { 534 localStorage.setItem('sp_activity', JSON.stringify(data.activity)); 535 logger.log('[Supabase] ð Restored activity calendar from cloud'); 536 } 537 538 localStorage.setItem('sp_last_cloud_sync', Date.now()); 539 540 // CRITICAL: Clear migration flag so My Library re-syncs if needed 541 // This ensures all cloud-synced lessons get added to the activity calendar 542 localStorage.removeItem('sp_activity_migrated'); 543 logger.log('[Supabase] ð Cleared migration flag - My Library will re-sync with cloud data on next visit'); 544 545 logger.log('[Supabase] â Progress loaded from cloud successfully!'); 546 logger.log(`[Supabase] â Restored ${completedLessons} completed lessons`); 547 } 548 549 return { success: true, data }; 550 } catch (error) { 551 console.error('[Supabase] â Load error:', error); 552 return { success: false, error: error.message }; 553 } 554 } 555 556 // Auto-sync progress periodically 557 function startAutoSync() { 558 if (!supabase || !currentUser) return; 559 560 // Sync every 5 minutes 561 setInterval(() => { 562 if (currentUser) { 563 syncProgressToCloud(); 564 } 565 }, 5 * 60 * 1000); 566 567 // Sync on page unload 568 window.addEventListener('beforeunload', () => { 569 if (currentUser) { 570 syncProgressToCloud(); 571 } 572 }); 573 } 574 575 // ========== UI UPDATES ========== 576 577 // Create and inject auth UI elements into header 578 function createAuthUIElements() { 579 const headerCtls = document.querySelector('.header-ctls'); 580 if (!headerCtls) { 581 logger.log('[Supabase] No header-ctls found, skipping auth UI'); 582 return; 583 } 584 585 // Check if already created 586 if (document.getElementById('auth-button')) { 587 return; // Already exists 588 } 589 590 // Create auth button 591 const authBtn = document.createElement('button'); 592 authBtn.id = 'auth-button'; 593 authBtn.className = 'btn btn-primary btn-sm'; 594 authBtn.textContent = 'Sign In'; 595 authBtn.onclick = showAuthModal; 596 597 // Insert before menu toggle (so it appears before it on mobile) 598 const menuToggle = headerCtls.querySelector('.menu-toggle'); 599 if (menuToggle) { 600 headerCtls.insertBefore(authBtn, menuToggle); 601 } else { 602 headerCtls.appendChild(authBtn); 603 } 604 605 logger.log('[Supabase] Auth UI elements created'); 606 } 607 608 function updateAuthUI(user) { 609 // Update auth button 610 const authBtn = document.getElementById('auth-button'); 611 if (authBtn) { 612 if (user) { 613 const userName = user.user_metadata?.user_name || user.email.split('@')[0]; 614 // Create initials (e.g., "Archi Rasca" -> "A.R") 615 const initials = userName.split(' ') 616 .map(word => word.charAt(0).toUpperCase()) 617 .join('.') 618 .substring(0, 5); // Max 5 chars (X.X.X) 619 authBtn.textContent = initials; 620 // Use arrow function to ensure proper scope 621 authBtn.onclick = (e) => { 622 e.preventDefault(); 623 e.stopPropagation(); 624 logger.log('[Supabase] Auth button clicked, showing user menu...'); 625 showUserMenu(); 626 }; 627 } else { 628 authBtn.textContent = 'Sign In'; 629 authBtn.onclick = showAuthModal; 630 } 631 } 632 633 } 634 635 // Show sync error notification 636 function showSyncError(message) { 637 // Create error notification 638 const notification = document.createElement('div'); 639 notification.className = 'sync-error-notification'; 640 notification.innerHTML = ` 641 <div style="background: rgba(220, 38, 38, 0.95); color: white; padding: 1rem 1.5rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); max-width: 500px; margin: 1rem;"> 642 <div style="display: flex; align-items: start; gap: 0.75rem;">
643 <span style="font-size: 1.5rem;">â ï¸</span> 644 <div style="flex: 1;"> 645 <strong style="display: block; margin-bottom: 0.5rem;">Cloud Sync Failed</strong> 646 <p style="margin: 0; font-size: 0.9rem; opacity: 0.95;">${message}</p> 647 <p style="margin: 0.5rem 0 0 0; font-size: 0.85rem; opacity: 0.9;"> 648 Check browser console (F12) for details. 649 </p> 650 </div> 651 <button onclick="this.closest('.sync-error-notification').remove()" style="background: none; border: none; color: white; cursor: pointer; font-size: 1.2rem; padding: 0; opacity: 0.8;">â</button> 652 </div> 653 </div> 654 `; 655 notification.style.position = 'fixed'; 656 notification.style.top = '80px'; 657 notification.style.right = '20px'; 658 notification.style.zIndex = '9999'; 659 notification.style.animation = 'slideInRight 0.3s ease-out'; 660 661 document.body.appendChild(notification); 662 663 // Auto-remove after 10 seconds 664 setTimeout(() => { 665 if (notification.parentNode) { 666 notification.remove(); 667 } 668 }, 10000); 669 670 // Update sync indicator to show error 671 const syncIndicator = document.getElementById('cloud-sync-indicator'); 672 if (syncIndicator) { 673 syncIndicator.innerHTML = '<span class="sync-icon" style="filter: grayscale(100%);">âï¸</span>'; 674 syncIndicator.title = 'Cloud sync failed - click for details'; 675 syncIndicator.style.cursor = 'pointer'; 676 syncIndicator.onclick = () => { 677 alert(`Cloud Sync Error:\n\n${message}\n\nPlease check the browser console (F12) for more details.`); 678 }; 679 } 680 } 681 682 function showUserMenu() { 683 try { 684 console.log('[Supabase] 1. showUserMenu called'); 685 logger.log('[Supabase] 1. showUserMenu called'); 686 687 // Check if user is signed in 688 if (!currentUser) { 689 console.warn('[Supabase] No current user, cannot show menu'); 690 logger.warn('[Supabase] No current user, cannot show menu'); 691 return; 692 } 693 console.log('[Supabase] 2. Current user exists:', currentUser.email); 694 logger.log('[Supabase] 2. Current user exists:', currentUser.email); 695 696 // Remove any existing menu 697 const existingMenu = document.querySelector('.user-menu-dropdown'); 698 console.log('[Supabase] 3. Checked for existing menu:', !!existingMenu); 699 logger.log('[Supabase] 3. Checked for existing menu:', !!existingMenu); 700 701 if (existingMenu) { 702 console.log('[Supabase] Removing existing menu'); 703 logger.log('[Supabase] Removing existing menu'); 704 existingMenu.remove(); 705 return; // Toggle off if already open 706 } 707 708 console.log('[Supabase] 4. Creating user menu for:', currentUser.email); 709 logger.log('[Supabase] 4. Creating user menu for:', currentUser.email); 710 711 // Create dropdown menu with inline styles for guaranteed visibility 712 const menu = document.createElement('div'); 713 menu.className = 'user-menu-dropdown'; 714 menu.style.cssText = ` 715 position: fixed !important; 716 top: 70px !important; 717 right: 20px !important; 718 background: linear-gradient(135deg, rgba(10, 14, 26, 0.98), rgba(15, 20, 35, 0.98)) !important; 719 backdrop-filter: blur(12px) !important; 720 border: 1px solid rgba(91, 138, 255, 0.2) !important; 721 border-radius: 12px; 722 padding: 0.75rem; 723 min-width: 220px; 724 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(91, 138, 255, 0.1) !important; 725 z-index: 999999 !important; 726 display: block !important; 727 visibility: visible !important; 728 opacity: 1 !important; 729 animation: slideDown 0.2s ease; 730 `; 731 732 menu.innerHTML = ` 733 <style> 734 @keyframes slideDown { 735 from { transform: translateY(-10px); opacity: 0; } 736 to { transform: translateY(0); opacity: 1; } 737 } 738 .user-menu-header-info { 739 padding: 0.75rem; 740 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 741 margin-bottom: 0.5rem; 742 } 743 .user-menu-header-info strong { 744 display: block; 745 color: var(--text, #ecf1ff); 746 font-size: 1rem; 747 margin-bottom: 0.25rem; 748 } 749 .user-menu-header-info p { 750 color: var(--muted, #b7c2d9); 751 font-size: 0.85rem; 752 margin: 0; 753 } 754 .user-menu-btn { 755 display: flex; 756 align-items: center; 757 gap: 0.75rem; 758 width: 100%; 759 padding: 0.75rem; 760 border: none; 761 background: transparent; 762 color: var(--text, #ecf1ff); 763 text-align: left; 764 border-radius: 8px; 765 cursor: pointer; 766 transition: background 0.2s ease; 767 font-size: 0.95rem; 768 font-family: inherit; 769 } 770 .user-menu-btn:hover { 771 background: rgba(255, 255, 255, 0.08); 772 } 773 .user-menu-btn.danger:hover { 774 background: rgba(239, 68, 68, 0.1); 775 color: #ef4444; 776 } 777 </style> 778 <div class="user-menu-header-info"> 779 <strong>${currentUser.user_metadata?.user_name || 'User'}</strong> 780 <p>${currentUser.email}</p> 781 <p style="font-size: 0.85rem; margin-top: 0.5rem; padding: 0.5rem; background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(59, 130, 246, 0.2)); border-radius: 6px; display: flex; align-items: center; justify-content: space-between;">
782 <span><strong>Level ${Math.floor((parseInt(localStorage.getItem('sp_total_xp') || '0')) / 500) + 1}</strong></span> 783 <span style="opacity: 0.8;">${localStorage.getItem('sp_total_xp') || '0'} XP</span> 784 </p> 785 <p style="font-size: 0.75rem; opacity: 0.7; margin-top: 0.5rem;">âï¸ Auto-syncing to cloud</p> 786 </div> 787 <button class="user-menu-btn" onclick="window.location.href='/education/my-library.html'; event.stopPropagation();"> 788 ð <span>My Library</span> 789 </button> 790 <button class="user-menu-btn danger" onclick="if(confirm('Sign out and reload page?')) { window.supabaseAuth.signOut(); } event.stopPropagation();"> 791 ðª <span>Sign Out</span> 792 </button> 793 `; 794 795 console.log('[Supabase] 5. Menu HTML created, appending to body...'); 796 logger.log('[Supabase] 5. Menu HTML created, appending to body...'); 797 document.body.appendChild(menu); 798 console.log('[Supabase] 6. Menu appended! Should be visible now.'); 799 logger.log('[Supabase] 6. Menu appended! Should be visible now.'); 800 801 // Verify it was added 802 const verify = document.querySelector('.user-menu-dropdown'); 803 console.log('[Supabase] 7. Verification - menu in DOM:', !!verify); 804 logger.log('[Supabase] 7. Verification - menu in DOM:', !!verify); 805 806 // Close on click outside 807 setTimeout(() => { 808 const closeHandler = (e) => { 809 if (!menu.contains(e.target) && !e.target.closest('#auth-button')) { 810 menu.remove(); 811 document.removeEventListener('click', closeHandler); 812 } 813 }; 814 document.addEventListener('click', closeHandler); 815 }, 100); 816 817 } catch (error) { 818 console.error('[Supabase] ERROR in showUserMenu:', error); 819 logger.log('[Supabase] Full error:', error.stack); 820 } 821 } 822 823 function showAuthModal(mode = 'signin') { 824 // This will be implemented in auth-ui.js 825 if (typeof window.showAuthModal === 'function') { 826 window.showAuthModal(mode); 827 } 828 } 829 830 // ========== INITIALIZATION ========== 831 832 async function init() { 833 try { 834 // Create auth UI elements first 835 createAuthUIElements(); 836 837 await initSupabase(); 838 839 if (supabase) { 840 await initAuthListener(); 841 startAutoSync(); 842 logger.log('[Supabase] Fully initialized'); 843 } else { 844 logger.log('[Supabase] Running without cloud sync'); 845 } 846 } catch (error) { 847 console.error('[Supabase] Initialization error:', error); 848 } 849 } 850 851 // Start initialization when DOM is ready 852 if (document.readyState === 'loading') { 853 document.addEventListener('DOMContentLoaded', init); 854 } else { 855 init(); 856 } 857 858 // Helper: Trigger sync when progress changes 859 // Call this function after completing lessons, updating notes, etc. 860 function onProgressChange() { 861 if (currentUser && supabase) { 862 // Debounced sync - wait 2 seconds before syncing 863 clearTimeout(window._progressSyncTimer); 864 window._progressSyncTimer = setTimeout(() => { 865 logger.log('[Supabase] Progress changed, syncing to cloud...'); 866 syncProgressToCloud(); 867 }, 2000); 868 } 869 } 870 871 // Listen for localStorage changes (for progress updates) 872 window.addEventListener('storage', (e) => { 873 if (e.key && (e.key.startsWith('sp_edu_') || e.key.startsWith('sp_lesson') || e.key.startsWith('sp_learning') || 874 e.key.startsWith('sp_notes_') || 875 e.key === 'sp_bookmarks' || e.key === 'sp_favorites' || e.key === 'sp_downloads' || e.key === 'sp_activity' || 876 e.key === 'sp_total_xp' || e.key === 'sp_xp_log')) { 877 onProgressChange(); 878 } 879 }); 880 881 // Override localStorage.setItem to detect progress changes 882 const originalSetItem = localStorage.setItem; 883 localStorage.setItem = function(key, value) { 884 originalSetItem.apply(this, arguments); 885 886 // If progress-related key changed, trigger sync 887 if (key && (key.startsWith('sp_edu_') || key.startsWith('sp_lesson') || key.startsWith('sp_learning') || 888 key.startsWith('sp_notes_') || 889 key === 'sp_bookmarks' || key === 'sp_favorites' || key === 'sp_downloads' || key === 'sp_activity' || 890 key === 'sp_total_xp' || key === 'sp_xp_log')) { 891 onProgressChange(); 892 } 893 }; 894 895 // Export public API 896 window.supabaseAuth = { 897 signUp,
898 signIn, 899 signOut, 900 resetPassword, 901 getCurrentUser, 902 onAuthStateChange, 903 syncProgressToCloud, 904 loadProgressFromCloud, 905 onProgressChange, // Export for manual trigger 906 showUserMenu // Export for debugging/testing 907 }; 908 909 logger.log('[Supabase] Module loaded'); 910})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.