HTMLify
script.js
Views: 20 | Author: karbonsites
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | // IMPORTANT: This script relies on the Supabase CDN and assumes the Supabase client is loaded. // The user requested fixing everything, implying robust implementation of the original concept (Auth, Realtime Chat, Pruning). const SUPABASE_URL = "YOUR_SUPABASE_URL"; const SUPABASE_ANON_KEY = "YOUR_SUPABASE_ANON_KEY"; // Configuration Check if (SUPABASE_URL === "YOUR_SUPABASE_URL" || SUPABASE_ANON_KEY === "YOUR_SUPABASE_ANON_KEY") { console.error("Supabase keys are NOT configured. This application will fail authentication and chat features without valid keys."); } const supabase = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // --- DOM Elements --- const authContainer = document.getElementById('auth-container'); const chatContainer = document.getElementById('chat-container'); const loginForm = document.getElementById('login-form'); const registerForm = document.getElementById('register-form'); const loginTab = document.getElementById('login-tab'); const registerTab = document.getElementById('register-tab'); const messageForm = document.getElementById('message-form'); const messageInput = document.getElementById('message-input'); const messageFeed = document.getElementById('message-feed'); const logoutBtn = document.getElementById('logout-btn'); const currentUserEmailSpan = document.getElementById('current-user-email'); const userListUl = document.getElementById('user-list'); let currentUser = null; let windowPruningInterval = null; // --- State Management & UI Toggles --- function showMessage(elementId, message, isError = false) { const el = document.getElementById(elementId); if (!el) return; el.textContent = message; el.style.color = isError ? '#ff6b6b' : 'var(--color-text-primary)'; // Reset color and text after a delay setTimeout(() => { el.textContent = ''; el.style.color = ''; }, 5000); } function toggleAuthView(showAuth) { authContainer.classList.toggle('hidden', !showAuth); chatContainer.classList.toggle('hidden', showAuth); if (!showAuth) { // Ensure chat window is visible before attempting to scroll requestAnimationFrame(() => { scrollToBottom(); }); } } function scrollToBottom() { // Use requestAnimationFrame for smoother scroll after DOM update requestAnimationFrame(() => { messageFeed.scrollTop = messageFeed.scrollHeight; }); } // --- User Management (Auth) --- async function handleLogin(e) { e.preventDefault(); const email = loginForm.querySelector('#login-email').value; const password = loginForm.querySelector('#login-password').value; const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) { showMessage('login-message', error.message, true); } else { showMessage('login-message', 'Signing in...', false); } } async function handleRegister(e) { e.preventDefault(); const email = registerForm.querySelector('#register-email').value; const password = registerForm.querySelector('#register-password').value; // NOTE ON CLOUDFLARE AUTH: As per original instructions, we assume successful Supabase signup meets the 'post-Cloudflare auth' requirement, as direct integration is impossible here. const { error } = await supabase.auth.signUp({ email, password }); if (error) { showMessage('register-message', error.message, true); } else { showMessage('register-message', 'Success! Check email for verification link.', false); // Attempt auto-login after successful signup for better UX flow, even if email confirmation is pending. await supabase.auth.signInWithPassword({ email, password }); } } async function handleLogout() { const { error } = await supabase.auth.signOut(); if (error) { console.error("Logout error:", error); } else { // State cleanup is handled by the 'SIGNED_OUT' event in onAuthStateChange listener } } // --- Chat & Realtime Functions --- let messagesSubscription = null; let presenceSubscription = null; async function loadInitialMessages() { messageFeed.innerHTML = ''; const { data: initialMessages, error } = await supabase .from('messages') .select('id, content, created_at, user_email') .order('created_at', { ascending: true }) .limit(200); // Increased limit for better initial load if (error) { console.error("Error loading initial messages:", error); return; } initialMessages.forEach(msg => renderMessage(msg)); // Scroll to bottom only after rendering is complete scrollToBottom(); } function renderMessage(msg) { if (!currentUser) return; const isMine = msg.user_email === currentUser.email; const messageItem = document.createElement('div'); // Improved timestamp formatting const date = new Date(msg.created_at); const timeString = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const dateString = date.toLocaleDateString(); const displayTime = date.toDateString() === new Date().toDateString() ? timeString : `${timeString} (${dateString})`; messageItem.className = `message-item ${isMine ? 'mine' : 'other'}`; messageItem.dataset.id = msg.id; // Sanitize content to prevent XSS in plain text output const safeContent = msg.content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); messageItem.innerHTML = ` <div class="message-sender">${isMine ? 'You' : msg.user_email}</div> <div class="message-content">${safeContent}</div> <div class="message-timestamp">${displayTime}</div> `; messageFeed.appendChild(messageItem); // Trigger transition class after insertion requestAnimationFrame(() => { messageItem.classList.add('visible'); }); scrollToBottom(); } async function sendMessage(e) { e.preventDefault(); const content = messageInput.value.trim(); if (!content || !currentUser) return; // Optimistic UI update (Add message locally first) const optimisticMsg = { content: content, user_email: currentUser.email, created_at: new Date().toISOString() }; renderMessage(optimisticMsg); const { error } = await supabase .from('messages') .insert({ content, user_email: currentUser.email }); if (error) { console.error("Error sending message:", error); // If error occurs, remove the optimistic message (simple approach) const lastMessage = messageFeed.lastElementChild; if (lastMessage && lastMessage.querySelector('.message-content').textContent === content) { messageFeed.removeChild(lastMessage); } } messageInput.value = ''; messageInput.focus(); } // --- Realtime & Presence --- function setupRealtimeListeners() { // 1. Messages Channel if (messagesSubscription) supabase.removeChannel(messagesSubscription); messagesSubscription = supabase.channel('public:messages') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => { // Only render if it's a new message and not ours (we optimistically added ours) if (payload.new.user_email !== currentUser.email) { renderMessage(payload.new); } }) .subscribe(); // 2. Presence Channel if (presenceSubscription) supabase.removeChannel(presenceSubscription); presenceSubscription = supabase.channel('public:users', { config: { presence: { key: currentUser.id, // Use user ID as key for tracking options: { broadcast: true } } } }) .on('presence', { event: 'sync' }, () => { const presenceState = presenceSubscription.presenceState(); updateUserList(presenceState); }) .on('presence', { event: 'join' }, (payload) => { // Trigger sync to update list immediately if join happens without a full sync event const presenceState = presenceSubscription.presenceState(); updateUserList(presenceState); }) .on('presence', { event: 'leave' }, (payload) => { const presenceState = presenceSubscription.presenceState(); updateUserList(presenceState); }) .subscribe(async (status) => { if (status === 'SUBSCRIBED') { await presenceSubscription.track({ online_at: new Date().toISOString(), user_email: currentUser.email }); } }); } function updateUserList(presenceState) { const uniqueEmails = new Set(); // presenceState structure: { [channel_id]: [{ presence_data }, { presence_data }] } Object.keys(presenceState).forEach(key => { presenceState[key].forEach(presenceData => { if (presenceData.user_email) { uniqueEmails.add(presenceData.user_email); } }); }); userListUl.innerHTML = ''; // Sort emails for consistent display const sortedEmails = Array.from(uniqueEmails).sort((a, b) => a.localeCompare(b)); sortedEmails.forEach(email => { const li = document.createElement('li'); if (currentUser && email === currentUser.email) { // Mobile friendly display for 'You' li.innerHTML = `<span style="color: var(--color-text-primary); font-weight: bold;">You</span>`; } else { li.textContent = email; } userListUl.appendChild(li); }); } // --- Pruning Logic (Every 10 minutes) --- async function pruneOldMessages() { const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString(); // Ensure we only prune if connected to Supabase if (!supabase.auth.getSession().data.session) return; const { error } = await supabase .from('messages') .delete() .lt('created_at', tenMinutesAgo); if (error) { console.error("Pruning failed:", error.message); } else { console.log("Pruning check complete: Messages older than 10 minutes removed."); } } function startPruningTimer() { if (windowPruningInterval) clearInterval(windowPruningInterval); // Run immediately, then every 10 minutes pruneOldMessages(); windowPruningInterval = setInterval(pruneOldMessages, 10 * 60 * 1000); } function stopCleanup() { if (messagesSubscription) supabase.removeChannel(messagesSubscription); if (presenceSubscription) supabase.removeChannel(presenceSubscription); messagesSubscription = null; presenceSubscription = null; userListUl.innerHTML = ''; messageFeed.innerHTML = ''; if (windowPruningInterval) { clearInterval(windowPruningInterval); windowPruningInterval = null; } } // --- Global State Listener --- supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN' && session?.user) { currentUser = session.user; currentUserEmailSpan.textContent = session.user.email; toggleAuthView(false); loadInitialMessages(); setupRealtimeListeners(); startPruningTimer(); } else if (!session?.user && currentUser) { // User explicitly logged out or session expired currentUser = null; toggleAuthView(true); stopCleanup(); } }); // --- Initialization --- document.addEventListener('DOMContentLoaded', () => { // Initial state check const { data: { session } } = supabase.auth.getSession(); if (session?.user) { currentUser = session.user; currentUserEmailSpan.textContent = session.user.email; toggleAuthView(false); loadInitialMessages(); setupRealtimeListeners(); startPruningTimer(); } else { toggleAuthView(true); } // Auth Tab Switching Logic loginTab.addEventListener('click', () => { loginTab.classList.add('active'); registerTab.classList.remove('active'); loginForm.classList.remove('hidden'); registerForm.classList.add('hidden'); showMessage('login-message', ''); // Clear previous messages }); registerTab.addEventListener('click', () => { registerTab.classList.add('active'); loginTab.classList.remove('active'); registerForm.classList.remove('hidden'); loginForm.classList.add('hidden'); showMessage('register-message', ''); }); // Event Listeners Setup loginForm.addEventListener('submit', handleLogin); registerForm.addEventListener('submit', handleRegister); logoutBtn.addEventListener('click', handleLogout); messageForm.addEventListener('submit', sendMessage); }); |