diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
| commit | 76cb9c2a39d477a64824a985ade40507e3bbade1 (patch) | |
| tree | 41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/app.js | |
| parent | 819a39a21ac992b1393244a4c283bbb125208c69 (diff) | |
| download | neko-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.js | 143 |
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); + } + } + }); + } + } } } |
