aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/app.js
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
commit76cb9c2a39d477a64824a985ade40507e3bbade1 (patch)
tree41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/app.js
parent819a39a21ac992b1393244a4c283bbb125208c69 (diff)
downloadneko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/app.js')
-rw-r--r--vanilla/app.js143
1 files changed, 104 insertions, 39 deletions
diff --git a/vanilla/app.js b/vanilla/app.js
index f36037b..65a0833 100644
--- a/vanilla/app.js
+++ b/vanilla/app.js
@@ -15,23 +15,26 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
-async function fetchFeeds() {
+export async function fetchFeeds(apiBase = '') {
try {
- const response = await fetch('/api/feed/');
+ const response = await fetch(`${apiBase}/api/feed/`);
if (!response.ok) throw new Error('Failed to fetch feeds');
const feeds = await response.json();
renderFeeds(feeds);
+ return feeds;
} catch (err) {
console.error(err);
- document.getElementById('feeds-nav').innerHTML = '<div class="error">Error loading feeds</div>';
+ const nav = document.getElementById('feeds-nav');
+ if (nav) nav.innerHTML = '<div class="error">Error loading feeds</div>';
+ throw err;
}
}
-async function fetchItems(feedId = null, filter = null, query = null) {
+export async function fetchItems(feedId = null, filter = null, query = null, apiBase = '') {
const listEl = document.getElementById('entries-list');
- listEl.innerHTML = '<div class="loading">Loading items...</div>';
+ if (listEl) listEl.innerHTML = '<div class="loading">Loading items...</div>';
- let url = '/api/stream/';
+ let url = `${apiBase}/api/stream/`;
const params = new URLSearchParams();
if (feedId) params.append('feed_id', feedId);
if (filter === 'unread') params.append('read_filter', 'unread');
@@ -47,15 +50,22 @@ async function fetchItems(feedId = null, filter = null, query = null) {
if (!response.ok) throw new Error('Failed to fetch items');
const items = await response.json();
renderItems(items);
+ return items;
} catch (err) {
console.error(err);
- listEl.innerHTML = '<div class="error">Error loading items</div>';
+ if (listEl) listEl.innerHTML = '<div class="error">Error loading items</div>';
+ throw err;
}
}
-function renderFeeds(feeds) {
+export function renderFeeds(feeds) {
const nav = document.getElementById('feeds-nav');
+ if (!nav) return;
+
+ // Clear existing items but keep search container if present
+ const searchContainer = nav.querySelector('.search-container');
nav.innerHTML = '';
+ if (searchContainer) nav.appendChild(searchContainer);
const allLink = document.createElement('div');
allLink.className = 'feed-item';
@@ -63,7 +73,8 @@ function renderFeeds(feeds) {
allLink.onclick = () => {
document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
allLink.classList.add('active');
- document.getElementById('feed-title').textContent = 'All Items';
+ const title = document.getElementById('feed-title');
+ if (title) title.textContent = 'All Items';
fetchItems();
};
nav.appendChild(allLink);
@@ -74,7 +85,8 @@ function renderFeeds(feeds) {
unreadLink.onclick = () => {
document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
unreadLink.classList.add('active');
- document.getElementById('feed-title').textContent = 'Unread Items';
+ const title = document.getElementById('feed-title');
+ if (title) title.textContent = 'Unread Items';
fetchItems(null, 'unread');
};
nav.appendChild(unreadLink);
@@ -85,31 +97,36 @@ function renderFeeds(feeds) {
starredLink.onclick = () => {
document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
starredLink.classList.add('active');
- document.getElementById('feed-title').textContent = 'Starred Items';
+ const title = document.getElementById('feed-title');
+ if (title) title.textContent = 'Starred Items';
fetchItems(null, 'starred');
};
nav.appendChild(starredLink);
- feeds.forEach(feed => {
- const div = document.createElement('div');
- div.className = 'feed-item';
- div.textContent = feed.title || feed.url;
- div.title = feed.url;
- div.onclick = () => {
- document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
- div.classList.add('active');
- document.getElementById('feed-title').textContent = feed.title;
- fetchItems(feed.id);
- };
- nav.appendChild(div);
- });
+ if (Array.isArray(feeds)) {
+ feeds.forEach(feed => {
+ const div = document.createElement('div');
+ div.className = 'feed-item';
+ div.textContent = feed.title || feed.url;
+ div.title = feed.url;
+ div.onclick = () => {
+ document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+ div.classList.add('active');
+ const title = document.getElementById('feed-title');
+ if (title) title.textContent = feed.title;
+ fetchItems(feed.id);
+ };
+ nav.appendChild(div);
+ });
+ }
}
-function renderItems(items) {
+export function renderItems(items) {
const list = document.getElementById('entries-list');
+ if (!list) return;
list.innerHTML = '';
- if (items.length === 0) {
+ if (!items || items.length === 0) {
list.innerHTML = '<div class="empty">No items found.</div>';
return;
}
@@ -123,8 +140,8 @@ function renderItems(items) {
article.innerHTML = `
<header class="entry-header">
<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>
+ <button class="btn-star ${item.starred ? 'active' : ''}" data-id="${item.id}" data-starred="${item.starred}">${item.starred ? '★' : '☆'}</button>
+ <button class="btn-read ${item.read ? 'read' : 'unread'}" data-id="${item.id}" data-read="${item.read}">${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">
@@ -136,14 +153,22 @@ function renderItems(items) {
${item.description || ''}
</div>
`;
+
+ // Add event listeners programmatically to avoid inline onclick with modules
+ const starBtn = article.querySelector('.btn-star');
+ starBtn.onclick = () => toggleStar(item.id, item.starred, starBtn);
+
+ const readBtn = article.querySelector('.btn-read');
+ readBtn.onclick = () => toggleRead(item.id, item.read, readBtn);
+
list.appendChild(article);
});
}
-async function toggleStar(id, currentStatus, btn) {
+export async function toggleStar(id, currentStatus, btn, apiBase = '') {
const newStatus = !currentStatus;
try {
- const response = await fetch(`/api/item/${id}`, {
+ const response = await fetch(`${apiBase}/api/item/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, starred: newStatus })
@@ -153,17 +178,23 @@ async function toggleStar(id, currentStatus, btn) {
// Update UI
btn.textContent = newStatus ? '★' : '☆';
btn.classList.toggle('active');
- btn.onclick = () => toggleStar(id, newStatus, btn);
+ btn.onclick = () => toggleStar(id, newStatus, btn, apiBase);
+
+ // Update data attributes
+ btn.dataset.starred = newStatus;
+
+ return newStatus;
} catch (err) {
console.error(err);
alert('Error toggling star');
+ throw err;
}
}
-async function toggleRead(id, currentStatus, btn) {
+export async function toggleRead(id, currentStatus, btn, apiBase = '') {
const newStatus = !currentStatus;
try {
- const response = await fetch(`/api/item/${id}`, {
+ const response = await fetch(`${apiBase}/api/item/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, read: newStatus })
@@ -174,19 +205,53 @@ async function toggleRead(id, currentStatus, btn) {
btn.textContent = newStatus ? 'Mark Unread' : 'Mark Read';
btn.classList.toggle('read');
btn.classList.toggle('unread');
- btn.onclick = () => toggleRead(id, newStatus, btn);
+ btn.onclick = () => toggleRead(id, newStatus, btn, apiBase);
+
+ // Update data attributes
+ btn.dataset.read = newStatus;
// 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');
+ if (header) {
+ const title = header.querySelector('.entry-title');
+ if (title) {
+ if (newStatus) {
+ title.classList.add('read');
+ } else {
+ title.classList.remove('read');
+ }
+ }
}
+ return newStatus;
} catch (err) {
console.error(err);
alert('Error toggling read status');
+ throw err;
+ }
+}
+
+export function init() {
+ if (typeof document !== 'undefined') {
+ // Only run if we're in a browser environment with these elements
+ if (document.getElementById('feeds-nav')) {
+ fetchFeeds();
+ fetchItems();
+
+ const searchInput = document.getElementById('search-input');
+ if (searchInput) {
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ const query = searchInput.value.trim();
+ if (query) {
+ const title = document.getElementById('feed-title');
+ if (title) title.textContent = `Search: ${query}`;
+ document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+ fetchItems(null, null, query);
+ }
+ }
+ });
+ }
+ }
}
}