From 7f004104e21df0d4ddef52776172a27ad882e0fc Mon Sep 17 00:00:00 2001 From: King Omar Date: Sat, 27 Jun 2026 03:26:17 +1000 Subject: [PATCH] Fix comments: remove 1s queue, direct fetch, optimistic rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The queue (COMMENT_REQUEST_DELAY=1000ms) was causing all comment loads to wait 1 second regardless of context. Comments are always user-triggered (toggle click, modal open, post submit) so batching was pointless overhead. - loadComments() now does a direct async fetch — no queue, no delay - submitComment() optimistically renders the new comment instantly (faded) then replaces with real server data once the POST confirms - Remove batchLoadComments, queueCommentLoad, updateCommentCount, and all queue machinery (commentRequestQueue, commentRequestTimeout, etc.) - Toggle arrow direction preserved during count updates - All containers updated via querySelectorAll (feed card + modal) Co-Authored-By: Claude Sonnet 4.6 --- index.html | 280 +++++++++++++++-------------------------------------- 1 file changed, 77 insertions(+), 203 deletions(-) diff --git a/index.html b/index.html index 0fd8982..fc28951 100644 --- a/index.html +++ b/index.html @@ -2423,12 +2423,41 @@ function toggleComments(proposalId, button) { } // Function to load comments for a proposal (feed cards and modals) -function loadComments(proposalId) { +async function loadComments(proposalId) { + // Show loading state immediately in all matching containers (feed + modal) document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { const content = container.querySelector('.comments-content'); - if (content) content.innerHTML = `
Loading comments...
`; + if (content) content.innerHTML = `
Loading...
`; }); - queueCommentLoad(proposalId); + + const userId = currentUser?.id || 'anonymous'; + try { + const res = await fetch( + `${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${userId}`, + { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, mode: 'cors', credentials: 'same-origin' } + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + + document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { + const content = container.querySelector('.comments-content'); + if (content) renderComments(content, data); + }); + + // Update toggle arrow + count, preserving open/closed arrow direction + const toggle = document.getElementById(`comments-toggle-${proposalId}`); + if (toggle) { + const arrow = toggle.querySelector('.comments-toggle-icon')?.textContent || '▼'; + const count = Array.isArray(data) ? data.length : 0; + toggle.innerHTML = `${arrow} ${count} ${count === 1 ? 'comment' : 'comments'}`; + } + } catch (err) { + console.error('Error loading comments:', err); + document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { + const content = container.querySelector('.comments-content'); + if (content) content.innerHTML = `
Could not load comments.
`; + }); + } } // Function to render comments @@ -2489,138 +2518,57 @@ function formatCommentText(text) { return withLinks; } -// Function to submit a new comment async function submitComment(proposalId, commentText) { - // If commentText is an element, extract the value (backward compatibility) - if (typeof commentText !== 'string') { - const form = commentText; - const commentInput = form.querySelector('.comment-input'); - commentText = commentInput.value.trim(); - - // Disable form while submitting - commentInput.disabled = true; - const submitButton = form.querySelector('.comment-submit'); - submitButton.disabled = true; - - // Enable form when done - const enableForm = () => { - commentInput.disabled = false; - submitButton.disabled = false; - }; - - // Return early if empty - if (!commentText) { - enableForm(); - return; - } - - // Clear input optimistically - commentInput.value = ''; - } - - // Ensure we have text to submit - if (!commentText || !commentText.trim()) { - console.error('Empty comment text'); - return; - } - - console.log(`Submitting comment for proposal ${proposalId}: ${commentText.substring(0, 20)}...`); - + if (!commentText || !commentText.trim()) return; + if (!currentUser || !currentUser.id) await loadUserData(); + + // Optimistically render the comment immediately so it appears instant + const optimisticComment = { + id: 'optimistic_' + Date.now(), + user_name: currentUser.name || 'You', + comment_text: commentText, + timestamp: Date.now(), + upvotes: 0, + downvotes: 0, + userVote: null + }; + document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { + const content = container.querySelector('.comments-content'); + if (!content) return; + const noComments = content.querySelector('.no-comments'); + if (noComments) content.innerHTML = ''; + const div = document.createElement('div'); + div.className = 'comment-item optimistic'; + div.style.opacity = '0.6'; + div.innerHTML = ` +
+
${optimisticComment.user_name}
+
just now
+
+
${formatCommentText(commentText)}
`; + content.prepend(div); + }); + try { - console.log("Making POST request to comments endpoint"); - - // Log the full API URL and request details - const requestUrl = `${API_BASE_URL}/comments`; - const requestData = { - proposalId: proposalId, - userId: currentUser?.id || 'anonymous', - commentText: commentText - }; - - console.log("Request URL:", requestUrl); - console.log("Request data:", requestData); - - // Ensure we have a user - if (!currentUser || !currentUser.id) { - console.error("No current user - creating anonymous"); - showToastMessage('Please wait, initializing user...', '#ff0099'); - await loadUserData(); // Reinitialize user - } - - // Create a controller to manage request timeouts - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(requestUrl, { + const res = await fetch(`${API_BASE_URL}/comments`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - proposalId: proposalId, - userId: currentUser.id, - commentText: commentText - }), + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ proposalId, userId: currentUser.id, commentText }), mode: 'cors', - credentials: 'same-origin', - signal: controller.signal + credentials: 'same-origin' }); - - clearTimeout(timeoutId); - - console.log("Comment submission response status:", response.status); - console.log("Response headers:", Object.fromEntries(response.headers.entries())); - - // Process response based on status code - if (response.status === 200 || response.status === 201) { - // Successfully created comment - try { - const data = await response.json(); - console.log("Comment created successfully:", data); - - loadComments(proposalId); - updateCommentCount(proposalId); - showToastMessage('Comment posted!', '#11cc77'); - return data; - } catch (jsonError) { - console.warn("Could not parse success response as JSON:", jsonError); - // Still consider this a success if status code was good - loadComments(proposalId); - updateCommentCount(proposalId); - showToastMessage('Comment posted!', '#11cc77'); - return { success: true }; - } - } else { - // Server returned an error - let errorMessage; - try { - const errorData = await response.json(); - console.error("Error response data:", errorData); - errorMessage = errorData.error || 'Unknown server error'; - } catch (e) { - try { - errorMessage = await response.text() || 'Failed to post comment'; - console.error("Error response text:", errorMessage); - } catch (textError) { - errorMessage = `HTTP error ${response.status}`; - console.error("Could not read error response:", textError); - } - } - - console.error('Error posting comment:', errorMessage); - showToastMessage('Failed to post comment', '#ff0099'); - throw new Error(errorMessage); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `HTTP ${res.status}`); } - } catch (error) { - if (error.name === 'AbortError') { - console.error('Comment request timed out'); - showToastMessage('Comment request timed out', '#ff0099'); - } else { - console.error('Network or processing error:', error); - showToastMessage('Failed to post comment', '#ff0099'); - } - throw error; + // Replace optimistic with real data from server + loadComments(proposalId); + showToastMessage('Comment posted!', '#11cc77'); + } catch (err) { + // Remove the optimistic comment on failure + document.querySelectorAll('.comment-item.optimistic').forEach(el => el.remove()); + showToastMessage('Failed to post comment', '#ff0099'); + throw err; } } @@ -2890,80 +2838,6 @@ document.addEventListener('DOMContentLoaded', () => { }); - // Add these functions outside your renderProposals function - -function updateCommentCount(proposalId) { - const commentsToggle = document.getElementById(`comments-toggle-${proposalId}`); - if (!commentsToggle) return; - - safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}`) - .then(comments => { - const commentCount = Array.isArray(comments) ? comments.length : 0; - const commentText = commentCount === 1 ? 'comment' : 'comments'; - commentsToggle.innerHTML = ` ${commentCount} ${commentText}`; - }) - .catch(() => { - commentsToggle.innerHTML = ` 0 comments`; - }); -} - - -// Add these new functions for comment management -const commentRequestQueue = new Set(); -let commentRequestTimeout = null; -const MAX_BATCH_SIZE = 10; -const COMMENT_REQUEST_DELAY = 1000; // 1 second delay between batches - -async function batchLoadComments() { - commentRequestTimeout = null; // reset so the next queueCommentLoad can schedule again - if (commentRequestQueue.size === 0) return; - - const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE); - commentRequestQueue.clear(); - - const userId = currentUser?.id || 'anonymous'; - - await Promise.all(batch.map(async proposalId => { - try { - const response = await fetch( - `${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${userId}`, - { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, mode: 'cors', credentials: 'same-origin' } - ); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - - // Update ALL containers for this proposal (covers both feed card and modal) - document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { - const content = container.querySelector('.comments-content'); - if (content) renderComments(content, data); - }); - - // Update toggle count on feed card - const toggle = document.getElementById(`comments-toggle-${proposalId}`); - if (toggle) { - const count = Array.isArray(data) ? data.length : 0; - toggle.innerHTML = ` ${count} ${count === 1 ? 'comment' : 'comments'}`; - } - } catch (err) { - console.error(`Failed to load comments for ${proposalId}:`, err); - document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => { - const content = container.querySelector('.comments-content'); - if (content) content.innerHTML = `
Could not load comments.
`; - }); - } - })); - - if (commentRequestQueue.size > 0) { - commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY); - } -} - -function queueCommentLoad(proposalId) { - commentRequestQueue.add(proposalId); - if (!commentRequestTimeout) { - commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY); - } -} // ─── STATS BAR ──────────────────────────────────────────────────────────── async function fetchStats() {