diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 20:47:45 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 20:47:45 -0800 |
| commit | d438ff62ad8ba890fbab5380785b723ed25afd34 (patch) | |
| tree | 41ebaef42f7ebfb6822098a599804a0b4da5398d | |
| parent | 408f88e4d26048396a01bf82e36eb133db3feb24 (diff) | |
| download | neko-d438ff62ad8ba890fbab5380785b723ed25afd34.tar.gz neko-d438ff62ad8ba890fbab5380785b723ed25afd34.tar.bz2 neko-d438ff62ad8ba890fbab5380785b723ed25afd34.zip | |
feat(vanilla): implement read/star toggle and filtering (NK-d4c8jv)
| -rw-r--r-- | vanilla/app.js | 92 | ||||
| -rw-r--r-- | vanilla/style.css | 39 |
2 files changed, 126 insertions, 5 deletions
diff --git a/vanilla/app.js b/vanilla/app.js index d1b2337..048a446 100644 --- a/vanilla/app.js +++ b/vanilla/app.js @@ -15,13 +15,18 @@ async function fetchFeeds() { } } -async function fetchItems(feedId = null) { +async function fetchItems(feedId = null, filter = null) { const listEl = document.getElementById('entries-list'); listEl.innerHTML = '<div class="loading">Loading items...</div>'; let url = '/api/stream/'; - if (feedId) { - url += `?feed_id=${feedId}`; + const params = new URLSearchParams(); + if (feedId) params.append('feed_id', feedId); + if (filter === 'unread') params.append('read_filter', 'unread'); + if (filter === 'starred') params.append('starred', 'true'); + + if ([...params].length > 0) { + url += '?' + params.toString(); } try { @@ -43,11 +48,35 @@ function renderFeeds(feeds) { allLink.className = 'feed-item'; allLink.textContent = 'All Items'; allLink.onclick = () => { + document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active')); + allLink.classList.add('active'); document.getElementById('feed-title').textContent = 'All Items'; fetchItems(); }; nav.appendChild(allLink); + const unreadLink = document.createElement('div'); + unreadLink.className = 'feed-item'; + unreadLink.textContent = 'Unread Items'; + unreadLink.onclick = () => { + document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active')); + unreadLink.classList.add('active'); + document.getElementById('feed-title').textContent = 'Unread Items'; + fetchItems(null, 'unread'); + }; + nav.appendChild(unreadLink); + + const starredLink = document.createElement('div'); + starredLink.className = 'feed-item'; + starredLink.textContent = 'Starred Items'; + starredLink.onclick = () => { + document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active')); + starredLink.classList.add('active'); + document.getElementById('feed-title').textContent = 'Starred Items'; + fetchItems(null, 'starred'); + }; + nav.appendChild(starredLink); + feeds.forEach(feed => { const div = document.createElement('div'); div.className = 'feed-item'; @@ -80,7 +109,11 @@ function renderItems(items) { article.innerHTML = ` <header class="entry-header"> - <a href="${item.url}" class="entry-title" target="_blank">${item.title}</a> + <div class="entry-controls"> + <button class="btn-star ${item.starred ? 'active' : ''}" onclick="toggleStar(${item.id}, ${item.starred}, this)">${item.starred ? '★' : '☆'}</button> + <button class="btn-read ${item.read ? 'read' : 'unread'}" onclick="toggleRead(${item.id}, ${item.read}, this)">${item.read ? 'Mark Unread' : 'Mark Read'}</button> + </div> + <a href="${item.url}" class="entry-title ${item.read ? 'read' : ''}" target="_blank">${item.title}</a> <div class="entry-meta"> ${item.feed ? `<span class="feed-name">${item.feed.title}</span> • ` : ''} <span class="date">${date}</span> @@ -93,3 +126,54 @@ function renderItems(items) { list.appendChild(article); }); } + +async function toggleStar(id, currentStatus, btn) { + const newStatus = !currentStatus; + try { + const response = await fetch(`/api/item/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id, starred: newStatus }) + }); + if (!response.ok) throw new Error('Failed to toggle star'); + + // Update UI + btn.textContent = newStatus ? '★' : '☆'; + btn.classList.toggle('active'); + btn.onclick = () => toggleStar(id, newStatus, btn); + } catch (err) { + console.error(err); + alert('Error toggling star'); + } +} + +async function toggleRead(id, currentStatus, btn) { + const newStatus = !currentStatus; + try { + const response = await fetch(`/api/item/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id, read: newStatus }) + }); + if (!response.ok) throw new Error('Failed to toggle read'); + + // Update UI + btn.textContent = newStatus ? 'Mark Unread' : 'Mark Read'; + btn.classList.toggle('read'); + btn.classList.toggle('unread'); + btn.onclick = () => toggleRead(id, newStatus, btn); + + // Find title and dim it if read + const header = btn.closest('.entry-header'); + const title = header.querySelector('.entry-title'); + if (newStatus) { + title.classList.add('read'); + } else { + title.classList.remove('read'); + } + + } catch (err) { + console.error(err); + alert('Error toggling read status'); + } +} diff --git a/vanilla/style.css b/vanilla/style.css index f83011f..729787b 100644 --- a/vanilla/style.css +++ b/vanilla/style.css @@ -96,6 +96,34 @@ body { .entry-header { margin-bottom: 0.5rem; + position: relative; + padding-left: 0; +} + +.entry-controls { + display: inline-block; + vertical-align: middle; + margin-right: 0.5rem; +} + +.btn-star, +.btn-read { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + padding: 2px 4px; + margin-right: 4px; + color: #ccc; +} + +.btn-star.active { + color: orange; +} + +.btn-star:hover, +.btn-read:hover { + color: #888; } .entry-title { @@ -103,13 +131,22 @@ body { font-weight: bold; color: var(--link-color); text-decoration: none; - display: block; + display: inline-block; + /* Changed to inline-block for alignment */ + vertical-align: middle; margin-bottom: 0.25rem; } +.entry-title.read { + font-weight: normal; + color: #555; + text-decoration: none; +} + .entry-meta { font-size: 0.85rem; color: #666; + margin-top: 4px; } .entry-content { |
