diff --git a/src/js/like-button.js b/src/js/like-button.js index 3a0a0f9..0b71e5d 100644 --- a/src/js/like-button.js +++ b/src/js/like-button.js @@ -1,3 +1,30 @@ +/** + * Helper function to handle fetch requests, parse the response, and manage common errors. + * @param {string} url - The URL to fetch. + * @param {object} options - The options for the fetch request. + * @returns {Promise} - A promise that resolves to the JSON response data. + */ +const fetchApiData = async (url, options = {}) => { + const response = await fetch(url, options); + + if (!response.ok) { + let errorMessage; + // Specifically handle the "Too Many Requests" error + if (response.status === 429) { + // Try to get the plain text message from the response body + const errorText = await response.text(); + errorMessage = errorText || 'Too many requests. Please try again later.'; + } else { + errorMessage = `Server responded with status ${response.status}`; + } + // Throw an error with our improved message + throw new Error(errorMessage); + } + + // If the response is OK, try to parse it as JSON. + return response.json(); +}; + document.addEventListener('DOMContentLoaded', () => { const likeIcon = document.querySelector('[data-like-button]'); if (!likeIcon) return; @@ -28,9 +55,7 @@ document.addEventListener('DOMContentLoaded', () => { // Fetch and display the initial like count and set the initial state const getInitialState = async () => { try { - const response = await fetch(`/api/likes/${slug}`); - if (!response.ok) return; - const data = await response.json(); + const data = await fetchApiData(`/api/likes/${slug}`); const isLiked = localStorage.getItem(storageKey) === 'true'; updateUI(isLiked, data.count || 0); // Check if the user has ever interacted with this button before @@ -43,7 +68,7 @@ document.addEventListener('DOMContentLoaded', () => { }, 2000); // Animation duration is 2s } } catch (error) { - console.error('Error fetching likes:', error); + console.error(`Error fetching initial like state for slug ${slug}:`, error.message); } }; @@ -62,20 +87,15 @@ document.addEventListener('DOMContentLoaded', () => { const method = isCurrentlyLiked ? 'DELETE' : 'POST'; try { - const response = await fetch(`/api/likes/${slug}`, { method }); - if (!response.ok) { - throw new Error(`Server responded with ${response.status}`); - } - const data = await response.json(); + const data = await fetchApiData(`/api/likes/${slug}`, { method }); const isNowLiked = !isCurrentlyLiked; // Update localStorage to remember the user's choice localStorage.setItem(storageKey, isNowLiked); // Update the button and count on the page updateUI(isNowLiked, data.count); - } catch (error) { - console.error('Error submitting like/unlike:', error); + console.error(`Error submitting like/unlike for slug ${slug}:`, error.message); } finally { isRequestInProgress = false; } diff --git a/src/js/view-counter.js b/src/js/view-counter.js index 1ebc8bd..a17e4ae 100644 --- a/src/js/view-counter.js +++ b/src/js/view-counter.js @@ -1,72 +1,70 @@ +/** + * Helper function to handle fetch requests, parse the response, and manage common errors. + * @param {string} url - The URL to fetch. + * @param {object} options - The options for the fetch request. + * @returns {Promise} - A promise that resolves to the JSON response data. + */ +const fetchViewData = async (url, options = {}) => { + const response = await fetch(url, options); + + if (!response.ok) { + let errorMessage; + // Specifically handle the "Too Many Requests" error + if (response.status === 429) { + // Try to get the plain text message from the response body + const errorText = await response.text(); + errorMessage = errorText || 'Too many requests. Please try again later.'; + } else { + errorMessage = `Server responded with status ${response.status}`; + } + // Throw an error with our improved message + throw new Error(errorMessage); + } + + // If the response is OK, try to parse it as JSON. + // A SyntaxError here would indicate an API problem (e.g., returning non-JSON on a 200 OK). + return response.json(); +}; + document.addEventListener('DOMContentLoaded', () => { // Select all elements that are designated to display view counts. const viewCountElements = document.querySelectorAll('[data-view-count]'); // If no such elements are found, do nothing. - if (viewCountElements.length === 0) { - return; - } + if (viewCountElements.length === 0) return; // Process each view count element found on the page. - viewCountElements.forEach(element => { + viewCountElements.forEach(async (element) => { const slug = element.dataset.slug; - if (!slug) { - // Skip if the element is missing the data-slug attribute. - return; - } + if (!slug) return; // Check if the element is on a single post page by looking for a `.post-meta` parent. // This distinguishes a single post view (which should be counted) from a list view. const isSinglePost = element.closest('.post-meta'); + const shouldIncrement = isSinglePost && !localStorage.getItem(`viewed-${slug}`); + const apiUrl = `/api/views/${slug}`; - if (isSinglePost) { - // On a single post page, increment the view count. - const viewed = localStorage.getItem(`viewed-${slug}`); + try { + const options = shouldIncrement ? { method: 'POST' } : {}; + const data = await fetchViewData(apiUrl, options); - // Only increment if the user hasn't viewed this post in the current session. - if (!viewed) { - fetch(`/api/views/${slug}`, { method: 'POST' }) - .then(response => { - if (!response.ok) throw new Error('Network response was not ok'); - return response.json(); - }) - .then(data => { - const count = data.count ?? 'Error'; - element.textContent = `${count} ${count === 1 ? 'view' : 'views'}`; - // Mark as viewed to prevent re-counting on refresh. - localStorage.setItem(`viewed-${slug}`, 'true'); - }) - .catch(error => { - console.error(`Error incrementing view count for slug ${slug}:`, error); - element.textContent = 'N/A'; - }); - } else { - // If already viewed in this session, just fetch the count without incrementing. - fetch(`/api/views/${slug}`) - .then(response => { - if (!response.ok) throw new Error('Network response was not ok'); - return response.json(); - }) - .then(data => { - const count = data.count ?? 0; - element.textContent = `${count} ${count === 1 ? 'view' : 'views'}`; - }) - .catch(error => { - console.error(`Error fetching view count for slug ${slug}:`, error); - element.textContent = 'N/A'; - }); + if (shouldIncrement) { + // Mark as viewed to prevent re-counting on refresh. + localStorage.setItem(`viewed-${slug}`, 'true'); } - } else { - // On a list page (like the main blog page), just fetch and display the count. - fetch(`/api/views/${slug}`) - .then(response => response.json()) - .then(data => { - element.textContent = data.count ?? 0; - }) - .catch(error => { - console.error(`Error fetching view count for slug ${slug}:`, error); - element.textContent = 'N/A'; - }); + + // Update UI based on context (single post vs. list) + if (isSinglePost) { + // On a post, show "N views". Use 'Error' as a fallback if incrementing returns bad data. + const count = data.count ?? (shouldIncrement ? 'Error' : 0); + element.textContent = `${count} ${count === 1 ? 'view' : 'views'}`; + } else { + // On a list, just show the number. + element.textContent = data.count ?? 0; + } + } catch (error) { + console.error(`Error processing view count for slug ${slug}:`, error.message); + element.textContent = 'N/A'; } }); }); \ No newline at end of file