This commit is contained in:
Omar Najjar 2025-04-13 02:27:50 +10:00
parent ecf43500a1
commit 95a66ea6b0
3 changed files with 190 additions and 2457 deletions

File diff suppressed because it is too large Load diff

View file

@ -596,7 +596,7 @@ async function loadUserData() {
}, 3000);
}
// Improved fetch implementation
// Improved safeFetch implementation with Cloudflare error handling
async function safeFetch(url, options = {}) {
try {
// Ensure URL is a string
@ -607,7 +607,6 @@ async function safeFetch(url, options = {}) {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add any additional default headers
},
mode: 'cors',
credentials: 'same-origin'
@ -642,14 +641,26 @@ async function safeFetch(url, options = {}) {
// Check for successful response
if (!response.ok) {
const errorBody = await response.text();
console.error('Fetch Error Response:', {
status: response.status,
statusText: response.statusText,
body: errorBody
});
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
// Check if this is a Cloudflare error
if (errorText.includes('Cloudflare') || errorText.includes('cf-error')) {
console.error('Cloudflare Error:', {
status: response.status,
statusText: response.statusText,
body: errorText
});
// Return a special error object for Cloudflare errors
return {
error: true,
cloudflareError: true,
status: response.status,
message: 'The server is temporarily unavailable. Please try again later.'
};
}
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
return await response.json();
@ -671,7 +682,7 @@ async function safeFetch(url, options = {}) {
// Fetch proposals from API
// Updated fetchProposals with fallback mechanism
async function fetchProposals() {
try {
// Show loading indicator
@ -680,14 +691,38 @@ async function safeFetch(url, options = {}) {
const sortSelect = document.getElementById('sort-select');
const sortBy = sortSelect ? sortSelect.value : 'newest';
const data = await safeFetch(
`${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`
);
// Log the full request URL and parameters
const requestUrl = `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`;
console.log('Fetching proposals from:', requestUrl);
console.log('Current user:', currentUser);
const response = await fetch(requestUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'same-origin'
});
// Log the response details
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
if (!response.ok) {
const errorText = await response.text();
console.error('Error response:', errorText);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
const data = await response.json();
console.log('Proposals data:', data);
renderProposals(data);
} catch (error) {
console.error('Error fetching proposals:', error);
showErrorMessage('Oops! Failed to load petitions. Try refreshing the page.');
showErrorMessage('Failed to load petitions. Please check the console for details.', true);
}
}
@ -2566,6 +2601,90 @@ function submitComment(proposalId, commentText) {
});
}
// 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() {
if (commentRequestQueue.size === 0) return;
const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE);
commentRequestQueue.clear();
try {
const responses = await Promise.all(
batch.map(proposalId =>
fetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'same-origin'
}).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json().then(data => ({ proposalId, data }));
})
)
);
responses.forEach(({ proposalId, data }) => {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
if (commentsContainer) {
renderComments(commentsContainer.querySelector('.comments-content'), data);
}
});
} catch (error) {
console.error('Error loading comments batch:', error);
// Retry failed requests
batch.forEach(proposalId => commentRequestQueue.add(proposalId));
}
// Schedule next batch if there are more requests
if (commentRequestQueue.size > 0) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
function queueCommentLoad(proposalId) {
commentRequestQueue.add(proposalId);
if (!commentRequestTimeout) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
// Update the loadComments function to use the queue
function loadComments(proposalId) {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
const commentsContent = commentsContainer?.querySelector('.comments-content');
if (!commentsContent) return;
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
// Queue the comment load
queueCommentLoad(proposalId);
}
// Update the loadModalComments function to use the queue
async function loadModalComments(proposalId) {
const commentsContent = document.getElementById(`modal-comments-content-${proposalId}`);
if (!commentsContent) return;
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
// Queue the comment load
queueCommentLoad(proposalId);
}
</script>
<div class="music-prompt" id="music-prompt">hit me</div>

184
worker.js
View file

@ -3,6 +3,7 @@ const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
};
// Handle OPTIONS request for CORS preflight
@ -1109,137 +1110,31 @@ async function healthCheck(env) {
async function getProposals(request, env) {
try {
const url = new URL(request.url);
const sortBy = url.searchParams.get('sortBy') || 'newest';
const page = parseInt(url.searchParams.get('page')) || 1;
const limit = Math.min(
parseInt(url.searchParams.get('limit')) || DEFAULT_PAGE_SIZE,
MAX_PAGE_SIZE
);
const offset = (page - 1) * limit;
// Log request details
console.log(`Getting proposals with sort: ${sortBy}`);
// Add to your SQL query
query += ` LIMIT ? OFFSET ?`;
let query = `
SELECT
p.id, p.text, p.timestamp, p.trending, p.meme_url,
u.name as author_name, u.id as author_id,
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes,
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes,
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures,
(SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners
FROM proposals p
JOIN users u ON p.author_id = u.id
`;
// Add sorting
if (sortBy === 'newest') {
query += ' ORDER BY p.timestamp DESC';
} else if (sortBy === 'popular') {
query += `
ORDER BY (
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') -
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote')
) DESC
`;
} else if (sortBy === 'controversial') {
query += `
ORDER BY (
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') +
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote')
) DESC
`;
} else if (sortBy === 'petitions') {
// New sorting option for most petitioned
query += `
ORDER BY (
(SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1)
) DESC
`;
}
// Add to your bind parameters
const proposals = await env.DB.prepare(query).bind(limit, offset).all();
console.log(`Executing query: ${query}`);
// Get user ID for checking existing votes
const userId = url.searchParams.get('userId');
let proposals;
try {
proposals = await env.DB.prepare(query).all();
console.log(`Retrieved ${proposals.results.length} proposals`);
} catch (dbError) {
return createResponse({
error: 'Failed to retrieve proposals',
details: logError(dbError, { action: 'get_proposals', query })
}, 500);
}
// If we have a userId, get their votes for these proposals
if (userId && proposals.results.length > 0) {
const proposalIds = proposals.results.map(p => p.id);
try {
// Use parameter binding with placeholders
let placeholderStr = '';
for (let i = 0; i < proposalIds.length; i++) {
placeholderStr += (i === 0 ? '?' : ',?');
}
const votesQuery = `
SELECT proposal_id, vote_type, is_petition
FROM votes
WHERE user_id = ? AND proposal_id IN (${placeholderStr})
`;
console.log(`Executing votes query for user ${userId} with ${proposalIds.length} proposals`);
const bindParams = [userId, ...proposalIds];
const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all();
console.log(`Retrieved ${votes.results.length} votes for user ${userId}`);
// Create a map of proposal_id to vote info
const voteMap = {};
for (const vote of votes.results) {
voteMap[vote.proposal_id] = {
voteType: vote.vote_type,
isPetition: vote.is_petition === 1
};
}
// Also get petition details status
const petitionQuery = `
SELECT proposal_id, verified
FROM petition_details
WHERE user_id = ? AND proposal_id IN (${placeholderStr})
`;
const petitionDetails = await env.DB.prepare(petitionQuery).bind(...bindParams).all();
const petitionMap = {};
for (const petition of petitionDetails.results) {
petitionMap[petition.proposal_id] = {
verified: petition.verified === 1
};
}
// Add user's vote and petition status to each proposal
for (const proposal of proposals.results) {
const voteInfo = voteMap[proposal.id];
proposal.userVote = voteInfo ? voteInfo.voteType : null;
proposal.userPetitionSigned = voteInfo ? voteInfo.isPetition : false;
proposal.userPetitionVerified = petitionMap[proposal.id] ? petitionMap[proposal.id].verified : false;
}
} catch (voteError) {
// Just log the error but continue - we'll return proposals without vote data
logError(voteError, {
action: 'get_user_votes',
userId,
proposalCount: proposalIds.length
});
console.log('Warning: Failed to retrieve user votes, continuing without vote data');
// Add pagination info to response
return createResponse({
data: proposals.results,
pagination: {
page,
limit,
total: proposals.results.length,
hasMore: proposals.results.length === limit
}
}
return createResponse(proposals.results);
});
} catch (error) {
return createResponse({
error: 'Failed to process proposals request',
details: logError(error, { action: 'get_proposals_outer' })
}, 500);
// ... error handling ...
}
}
@ -2375,4 +2270,39 @@ async function serveCustomizedHtml(proposalId, request, env) {
// Fall back to regular page if something goes wrong
return await fetch(request);
}
}
}
function getCacheControl(path) {
const cacheRules = {
'/api/health': 'public, max-age=3600',
'/api/proposals': 'public, max-age=300, stale-while-revalidate=60',
'/api/comments': 'public, max-age=60, stale-while-revalidate=30',
default: 'no-cache'
};
return cacheRules[path] || cacheRules.default;
}
// In your fetch handler
if (request.method === "GET") {
const cacheControl = getCacheControl(path);
response.headers.set('Cache-Control', cacheControl);
}
function validateRequest(request, requiredParams = []) {
const url = new URL(request.url);
const missingParams = requiredParams.filter(param => !url.searchParams.has(param));
if (missingParams.length > 0) {
return createResponse({
error: 'Missing required parameters',
missing: missingParams
}, 400);
}
return null;
}
// In your endpoint handlers
const validationError = validateRequest(request, ['userId']);
if (validationError) return validationError;