PageSourceSearch

https://www.signalpilot.io/education/assets/supabase-client.js

js signalpilot.io collected 2026-10-05 23:10:45 UTC 33,638 bytes, 910 lines download raw bytes

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.