aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/app.js
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 20:47:45 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 20:47:45 -0800
commitd438ff62ad8ba890fbab5380785b723ed25afd34 (patch)
tree41ebaef42f7ebfb6822098a599804a0b4da5398d /vanilla/app.js
parent408f88e4d26048396a01bf82e36eb133db3feb24 (diff)
downloadneko-d438ff62ad8ba890fbab5380785b723ed25afd34.tar.gz
neko-d438ff62ad8ba890fbab5380785b723ed25afd34.tar.bz2
neko-d438ff62ad8ba890fbab5380785b723ed25afd34.zip
feat(vanilla): implement read/star toggle and filtering (NK-d4c8jv)
Diffstat (limited to 'vanilla/app.js')
-rw-r--r--vanilla/app.js92
1 files changed, 88 insertions, 4 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');
+ }
+}