Fix comments: remove 1s queue, direct fetch, optimistic rendering
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 <noreply@anthropic.com>
This commit is contained in:
parent
89c548bb86
commit
7f004104e2
1 changed files with 77 additions and 203 deletions
280
index.html
280
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 = `<div class="comments-loading">Loading comments...</div>`;
|
||||
if (content) content.innerHTML = `<div class="comments-loading">Loading...</div>`;
|
||||
});
|
||||
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 = `<span class="comments-toggle-icon">${arrow}</span> ${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 = `<div class="no-comments">Could not load comments.</div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">${optimisticComment.user_name}</div>
|
||||
<div class="comment-time">just now</div>
|
||||
</div>
|
||||
<div class="comment-text">${formatCommentText(commentText)}</div>`;
|
||||
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 = `<span class="comments-toggle-icon">▶</span> ${commentCount} ${commentText}`;
|
||||
})
|
||||
.catch(() => {
|
||||
commentsToggle.innerHTML = `<span class="comments-toggle-icon">▶</span> 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 = `<span class="comments-toggle-icon">▼</span> ${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 = `<div class="no-comments">Could not load comments.</div>`;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue