diff options
Diffstat (limited to 'web/dist/vanilla')
| -rw-r--r-- | web/dist/vanilla/app.js | 257 | ||||
| -rw-r--r-- | web/dist/vanilla/index.html | 49 | ||||
| -rw-r--r-- | web/dist/vanilla/style.css | 173 |
3 files changed, 0 insertions, 479 deletions
diff --git a/web/dist/vanilla/app.js b/web/dist/vanilla/app.js deleted file mode 100644 index 65a0833..0000000 --- a/web/dist/vanilla/app.js +++ /dev/null @@ -1,257 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - fetchFeeds(); - fetchItems(); // Default to fetching recent items - - const searchInput = document.getElementById('search-input'); - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - const query = searchInput.value.trim(); - if (query) { - document.getElementById('feed-title').textContent = `Search: ${query}`; - document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active')); - fetchItems(null, null, query); - } - } - }); -}); - -export async function fetchFeeds(apiBase = '') { - try { - 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); - const nav = document.getElementById('feeds-nav'); - if (nav) nav.innerHTML = '<div class="error">Error loading feeds</div>'; - throw err; - } -} - -export async function fetchItems(feedId = null, filter = null, query = null, apiBase = '') { - const listEl = document.getElementById('entries-list'); - if (listEl) listEl.innerHTML = '<div class="loading">Loading items...</div>'; - - let url = `${apiBase}/api/stream/`; - 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 (query) params.append('q', query); - - if ([...params].length > 0) { - url += '?' + params.toString(); - } - - try { - const response = await fetch(url); - if (!response.ok) throw new Error('Failed to fetch items'); - const items = await response.json(); - renderItems(items); - return items; - } catch (err) { - console.error(err); - if (listEl) listEl.innerHTML = '<div class="error">Error loading items</div>'; - throw err; - } -} - -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'; - allLink.textContent = 'All Items'; - allLink.onclick = () => { - document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active')); - allLink.classList.add('active'); - const title = document.getElementById('feed-title'); - if (title) 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'); - const title = document.getElementById('feed-title'); - if (title) 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'); - const title = document.getElementById('feed-title'); - if (title) title.textContent = 'Starred Items'; - fetchItems(null, 'starred'); - }; - nav.appendChild(starredLink); - - 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); - }); - } -} - -export function renderItems(items) { - const list = document.getElementById('entries-list'); - if (!list) return; - list.innerHTML = ''; - - if (!items || items.length === 0) { - list.innerHTML = '<div class="empty">No items found.</div>'; - return; - } - - items.forEach(item => { - const article = document.createElement('article'); - article.className = 'entry'; - - const date = new Date(item.published_at || item.created_at).toLocaleString(); - - article.innerHTML = ` - <header class="entry-header"> - <div class="entry-controls"> - <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"> - ${item.feed ? `<span class="feed-name">${item.feed.title}</span> • ` : ''} - <span class="date">${date}</span> - </div> - </header> - <div class="entry-content"> - ${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); - }); -} - -export async function toggleStar(id, currentStatus, btn, apiBase = '') { - const newStatus = !currentStatus; - try { - const response = await fetch(`${apiBase}/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, apiBase); - - // Update data attributes - btn.dataset.starred = newStatus; - - return newStatus; - } catch (err) { - console.error(err); - alert('Error toggling star'); - throw err; - } -} - -export async function toggleRead(id, currentStatus, btn, apiBase = '') { - const newStatus = !currentStatus; - try { - const response = await fetch(`${apiBase}/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, apiBase); - - // Update data attributes - btn.dataset.read = newStatus; - - // Find title and dim it if read - const header = btn.closest('.entry-header'); - 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); - } - } - }); - } - } - } -} diff --git a/web/dist/vanilla/index.html b/web/dist/vanilla/index.html deleted file mode 100644 index c504f6f..0000000 --- a/web/dist/vanilla/index.html +++ /dev/null @@ -1,49 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Neko Reader (Vanilla)</title> - <link rel="stylesheet" href="style.css"> - <style> - /* Minimal reset for now, proper styles in style.css */ - body, - html { - margin: 0; - padding: 0; - height: 100%; - font-family: sans-serif; - } - </style> -</head> - -<body> - <div id="app"> - <aside id="sidebar"> - <header> - <h1>🐱 Neko</h1> - </header> - <nav id="feeds-nav"> - <div class="search-container"> - <input type="text" id="search-input" placeholder="Search items..." /> - </div> - <div class="loading">Loading feeds...</div> - </nav> - </aside> - <main id="main"> - <header id="main-header"> - <h2 id="feed-title">All Items</h2> - </header> - <div id="entries-list"> - <div class="loading">Loading items...</div> - </div> - </main> - </div> - <script type="module"> - import { init } from './app.js'; - document.addEventListener('DOMContentLoaded', init); - </script> -</body> - -</html>
\ No newline at end of file diff --git a/web/dist/vanilla/style.css b/web/dist/vanilla/style.css deleted file mode 100644 index 573248d..0000000 --- a/web/dist/vanilla/style.css +++ /dev/null @@ -1,173 +0,0 @@ -:root { - --bg-color: #f6f6f6; - --sidebar-bg: #eaeaea; - --item-bg: #fff; - --text-color: #222; - --link-color: #0000EE; - /* Standard blue link */ - --border-color: #ddd; - --selected-bg: #e8f0fe; -} - -body { - background-color: var(--bg-color); - color: var(--text-color); - overflow: hidden; - /* App container handles scrolling */ -} - -#app { - display: flex; - height: 100vh; -} - -#sidebar { - width: 250px; - background-color: var(--sidebar-bg); - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; -} - -#sidebar header { - padding: 1rem; - border-bottom: 1px solid var(--border-color); -} - -#sidebar h1 { - margin: 0; - font-size: 1.2rem; -} - -.search-container { - padding: 0.5rem; - border-bottom: 1px solid var(--border-color); -} - -#search-input { - width: 100%; - padding: 0.5rem; - border: 1px solid var(--border-color); - border-radius: 4px; - box-sizing: border-box; -} - -#feeds-nav { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.feed-item { - padding: 0.5rem; - cursor: pointer; - border-radius: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.feed-item:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.feed-item.active { - font-weight: bold; - background-color: rgba(0, 0, 0, 0.1); -} - -#main { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #fff; -} - -#main-header { - padding: 1rem; - border-bottom: 1px solid var(--border-color); - background-color: #fcfcfc; -} - -#main-header h2 { - margin: 0; - font-size: 1.5rem; -} - -#entries-list { - flex: 1; - overflow-y: auto; - padding: 1rem; -} - -.entry { - background-color: var(--item-bg); - border-bottom: 1px solid var(--border-color); - padding: 1rem 0; -} - -.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 { - font-size: 1.2rem; - font-weight: bold; - color: var(--link-color); - text-decoration: none; - 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 { - line-height: 1.6; - max-width: 800px; -} - -.entry-content img { - max-width: 100%; - height: auto; -}
\ No newline at end of file |
