// Fetch and display feeds async function fetchFeeds() { try { const response = await fetch('/feeds'); if (response.ok) { const feeds = await response.json(); return feeds; } else { console.error('Failed to load feeds:', response.status); return null; } } catch (error) { console.error('Error loading feeds:', error); return null; } } // Close any open feed menus function closeAllFeedMenus() { document.querySelectorAll('.feed-menu.show').forEach(menu => { menu.classList.remove('show'); }); } // Add click handler to close menus when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.feed-menu') && !e.target.closest('.feed-menu-button')) { closeAllFeedMenus(); } }); function formatDate(dateStr) { if (!dateStr) return 'Not available'; const date = new Date(dateStr); return date.toLocaleString(); } function renderFeedEntries(entries) { const mainContent = document.querySelector('.main-content'); mainContent.innerHTML = ''; entries.forEach(entry => { const entryDiv = document.createElement('div'); entryDiv.className = 'feed-entry'; const title = document.createElement('h2'); title.className = 'feed-entry-title'; const readToggle = document.createElement('button'); readToggle.className = 'read-toggle'; readToggle.title = entry.marked_read ? 'Mark as unread' : 'Mark as read'; readToggle.innerHTML = entry.marked_read ? '' : ''; readToggle.onclick = async () => { try { const response = await fetch(`/entries/${entry.id}/state`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ read: !entry.marked_read }) }); if (response.ok) { entry.marked_read = !entry.marked_read; readToggle.title = entry.marked_read ? 'Mark as unread' : 'Mark as read'; readToggle.innerHTML = entry.marked_read ? '' : ''; } else { console.error('Failed to update read state:', response.status); } } catch (error) { console.error('Failed to update read state:', error); } }; title.appendChild(readToggle); const titleLink = document.createElement('a'); titleLink.href = entry.link || '#'; titleLink.target = '_blank'; titleLink.textContent = entry.title; titleLink.onclick = () => { if (!entry.marked_read) { // Mark as read in the background, don't wait for it fetch(`/entries/${entry.id}/state`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ read: true }) }).then(response => { if (response.ok) { entry.marked_read = true; readToggle.title = 'Mark as unread'; readToggle.innerHTML = ''; } else { console.error('Failed to update read state:', response.status); } }).catch(error => { console.error('Failed to update read state:', error); }); } // Let the default link behavior happen immediately }; title.appendChild(titleLink); const meta = document.createElement('div'); meta.className = 'feed-entry-meta'; if (entry.published) { const published = document.createElement('span'); published.textContent = `Published: ${formatDate(entry.published)}`; meta.appendChild(published); } if (entry.updated) { const updated = document.createElement('span'); updated.textContent = `Updated: ${formatDate(entry.updated)}`; meta.appendChild(updated); } const summary = document.createElement('div'); summary.className = 'feed-entry-summary'; summary.textContent = entry.summary; entryDiv.appendChild(title); entryDiv.appendChild(meta); entryDiv.appendChild(summary); mainContent.appendChild(entryDiv); }); } function openFeed(feed) { const name = document.createElement('span'); name.className = 'feed-name'; name.textContent = feed.name; const spinner = document.createElement('div'); spinner.className = 'feed-spinner'; name.appendChild(spinner); name.onclick = async () => { name.classList.add('loading'); try { const response = await fetch(`/poll/${feed.feed_id}`, { method: 'POST' }); if (response.ok) { const data = await response.json(); console.log('Feed poll response:', data); renderFeedEntries(data.entries); // Update the top bar title const topBarTitle = document.querySelector('.top-bar-title'); topBarTitle.innerHTML = `RSS Reader - ${feed.name}`; // Update the browser document title document.title = `RSS Reader - ${feed.name}`; } else { console.error('Failed to poll feed:', response.status); } } catch (error) { console.error('Error polling feed:', error); } finally { name.classList.remove('loading'); } }; const menuButton = document.createElement('button'); menuButton.className = 'feed-menu-button'; menuButton.innerHTML = ''; menuButton.title = 'Feed options'; menuButton.onclick = (e) => { e.stopPropagation(); closeAllFeedMenus(); menu.classList.toggle('show'); }; const menu = document.createElement('div'); menu.className = 'feed-menu'; const copyItem = document.createElement('a'); copyItem.className = 'feed-menu-item copy'; copyItem.innerHTML = ' Copy feed URL'; copyItem.onclick = async (e) => { e.stopPropagation(); try { await navigator.clipboard.writeText(feed.url); // Briefly show success feedback const originalText = copyItem.innerHTML; copyItem.innerHTML = ' Copied!'; setTimeout(() => { copyItem.innerHTML = originalText; }, 1500); } catch (error) { console.error('Failed to copy URL:', error); } menu.classList.remove('show'); }; const deleteItem = document.createElement('a'); deleteItem.className = 'feed-menu-item delete'; deleteItem.innerHTML = ' Remove Feed'; deleteItem.onclick = async (e) => { e.stopPropagation(); if (confirm(`Are you sure you want to delete "${feed.name}"?`)) { try { const response = await fetch(`/feeds/${feed.feed_id}`, { method: 'DELETE', }); if (response.ok) { handleFeeds(); } else { console.error('Failed to delete feed:', response.status); } } catch (error) { console.error('Error deleting feed:', error); } } menu.classList.remove('show'); }; menu.appendChild(copyItem); menu.appendChild(deleteItem); name.appendChild(menuButton); name.appendChild(menu); return name; } async function handleFeeds() { const feeds = await fetchFeeds(); const feedList = document.getElementById('feedList'); if (feeds) { console.log('Loaded feeds:', feeds); feedList.innerHTML = ''; if (feeds.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.className = 'feed-empty'; emptyMessage.textContent = 'No feeds added yet'; feedList.appendChild(emptyMessage); } else { // Group feeds by category const feedsByCategory = {}; const uncategorizedFeeds = []; feeds.forEach(feed => { const category = feed.categorization.length > 0 ? feed.categorization[0] : null; if (category) { if (!feedsByCategory[category]) { feedsByCategory[category] = []; } feedsByCategory[category].push(feed); } else { uncategorizedFeeds.push(feed); } }); // Sort categories alphabetically, but keep "Uncategorized" at the end const sortedCategories = Object.keys(feedsByCategory).sort((a, b) => a.localeCompare(b)); sortedCategories.push(""); feedsByCategory[""] = uncategorizedFeeds; // Create category sections sortedCategories.forEach(category => { const categorySection = document.createElement('div'); categorySection.className = 'feed-category'; const categoryHeader = document.createElement('h3'); categoryHeader.className = 'feed-category-header'; categoryHeader.textContent = category; categorySection.appendChild(categoryHeader); // Add feeds for this category feedsByCategory[category].forEach(feed => { categorySection.appendChild(openFeed(feed)); }); feedList.appendChild(categorySection); }); } } else { feedList.innerHTML = '
Failed to load feeds
'; } } // Load feeds when page loads document.addEventListener('DOMContentLoaded', handleFeeds); // User menu functionality const userMenuButton = document.getElementById('userMenuButton'); const userMenuDropdown = document.getElementById('userMenuDropdown'); // Toggle menu on button click userMenuButton.addEventListener('click', (e) => { e.stopPropagation(); userMenuDropdown.classList.toggle('show'); }); // Close menu when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.user-menu')) { userMenuDropdown.classList.remove('show'); } }); // Prevent menu from closing when clicking inside it userMenuDropdown.addEventListener('click', (e) => { e.stopPropagation(); }); // Logout functionality document.getElementById('logoutButton').addEventListener('click', async () => { try { const response = await fetch('/logout', { method: 'POST', }); if (response.ok) { window.location.href = '/login'; } } catch (error) { console.error('Logout failed:', error); } }); // Modal functionality const modal = document.getElementById('addFeedModal'); const addFeedButton = document.getElementById('addFeedButton'); const cancelButton = document.getElementById('cancelAddFeed'); const confirmButton = document.getElementById('confirmAddFeed'); const addFeedForm = document.getElementById('addFeedForm'); const errorMessage = document.getElementById('feedErrorMessage'); const loadingMessage = document.getElementById('loadingMessage'); function showModal() { modal.classList.add('show'); errorMessage.style.display = 'none'; loadingMessage.style.display = 'none'; addFeedForm.reset(); confirmButton.disabled = false; } function hideModal() { modal.classList.remove('show'); errorMessage.style.display = 'none'; loadingMessage.style.display = 'none'; addFeedForm.reset(); confirmButton.disabled = false; } function showError(message) { errorMessage.textContent = message; errorMessage.style.display = 'block'; loadingMessage.style.display = 'none'; confirmButton.disabled = false; } function showLoading() { loadingMessage.style.display = 'block'; errorMessage.style.display = 'none'; confirmButton.disabled = true; } addFeedButton.addEventListener('click', showModal); cancelButton.addEventListener('click', hideModal); // Close modal when clicking outside modal.addEventListener('click', (e) => { if (e.target === modal) { hideModal(); } }); confirmButton.addEventListener('click', async () => { const url = document.getElementById('feedUrl').value.trim(); if (!url) { showError('Please enter a feed URL'); return; } showLoading(); try { const response = await fetch('/feeds', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }), }); if (response.ok) { hideModal(); // Refresh the feed list handleFeeds(); } else { switch (response.status) { case 409: showError('You already have this feed added'); break; case 422: showError('Invalid feed URL. Please make sure it\'s a valid RSS or Atom feed.'); break; default: showError('Failed to add feed. Please try again.'); } } } catch (error) { console.error('Add feed failed:', error); showError('An error occurred. Please try again.'); } });