x
This commit is contained in:
parent
ecf43500a1
commit
95a66ea6b0
3 changed files with 190 additions and 2457 deletions
2316
cloudflare worker.js
2316
cloudflare worker.js
File diff suppressed because it is too large
Load diff
147
index.html
147
index.html
|
|
@ -596,7 +596,7 @@ async function loadUserData() {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved fetch implementation
|
// Improved safeFetch implementation with Cloudflare error handling
|
||||||
async function safeFetch(url, options = {}) {
|
async function safeFetch(url, options = {}) {
|
||||||
try {
|
try {
|
||||||
// Ensure URL is a string
|
// Ensure URL is a string
|
||||||
|
|
@ -607,7 +607,6 @@ async function safeFetch(url, options = {}) {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// Add any additional default headers
|
|
||||||
},
|
},
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
|
|
@ -642,14 +641,26 @@ async function safeFetch(url, options = {}) {
|
||||||
|
|
||||||
// Check for successful response
|
// Check for successful response
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Fetch Error Response:', {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
body: errorBody
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
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() {
|
async function fetchProposals() {
|
||||||
try {
|
try {
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
|
|
@ -680,14 +691,38 @@ async function safeFetch(url, options = {}) {
|
||||||
const sortSelect = document.getElementById('sort-select');
|
const sortSelect = document.getElementById('sort-select');
|
||||||
const sortBy = sortSelect ? sortSelect.value : 'newest';
|
const sortBy = sortSelect ? sortSelect.value : 'newest';
|
||||||
|
|
||||||
const data = await safeFetch(
|
// Log the full request URL and parameters
|
||||||
`${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`
|
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);
|
renderProposals(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching proposals:', 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>
|
</script>
|
||||||
|
|
||||||
<div class="music-prompt" id="music-prompt">hit me</div>
|
<div class="music-prompt" id="music-prompt">hit me</div>
|
||||||
|
|
|
||||||
184
worker.js
184
worker.js
|
|
@ -3,6 +3,7 @@ const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle OPTIONS request for CORS preflight
|
// Handle OPTIONS request for CORS preflight
|
||||||
|
|
@ -1109,137 +1110,31 @@ async function healthCheck(env) {
|
||||||
async function getProposals(request, env) {
|
async function getProposals(request, env) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(request.url);
|
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
|
// Add to your SQL query
|
||||||
console.log(`Getting proposals with sort: ${sortBy}`);
|
query += ` LIMIT ? OFFSET ?`;
|
||||||
|
|
||||||
let query = `
|
// Add to your bind parameters
|
||||||
SELECT
|
const proposals = await env.DB.prepare(query).bind(limit, offset).all();
|
||||||
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
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Executing query: ${query}`);
|
// Add pagination info to response
|
||||||
|
return createResponse({
|
||||||
// Get user ID for checking existing votes
|
data: proposals.results,
|
||||||
const userId = url.searchParams.get('userId');
|
pagination: {
|
||||||
let proposals;
|
page,
|
||||||
|
limit,
|
||||||
try {
|
total: proposals.results.length,
|
||||||
proposals = await env.DB.prepare(query).all();
|
hasMore: proposals.results.length === limit
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return createResponse(proposals.results);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return createResponse({
|
// ... error handling ...
|
||||||
error: 'Failed to process proposals request',
|
|
||||||
details: logError(error, { action: 'get_proposals_outer' })
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2375,4 +2270,39 @@ async function serveCustomizedHtml(proposalId, request, env) {
|
||||||
// Fall back to regular page if something goes wrong
|
// Fall back to regular page if something goes wrong
|
||||||
return await fetch(request);
|
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;
|
||||||
Loading…
Add table
Reference in a new issue