aboutsummaryrefslogtreecommitdiffstats
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
parent408f88e4d26048396a01bf82e36eb133db3feb24 (diff)
downloadneko-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.js92
-rw-r--r--vanilla/style.css39
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 {