From c652ac6a2cd23ef29f48465be09c2b674783e8e9 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sun, 15 Feb 2026 17:44:55 -0800 Subject: Vanilla JS (v3): Implement 3-pane layout, item fetching, reading, and testing --- frontend-vanilla/src/main.ts | 241 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 219 insertions(+), 22 deletions(-) (limited to 'frontend-vanilla/src/main.ts') diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 6396b50..6846a67 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -1,24 +1,221 @@ -import './style.css' -import typescriptLogo from './typescript.svg' -import viteLogo from '/vite.svg' -import { setupCounter } from './counter.ts' - -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

+import './style.css'; +import { apiFetch } from './api'; +import { store } from './store'; +import { router } from './router'; +import type { Feed, Item } from './types'; +import { createFeedItem } from './components/FeedItem'; + +// Cache elements +const appEl = document.querySelector('#app')!; + +// Initial Layout +appEl.innerHTML = ` +
+ +
+
+

All Items

+
+
+
+
+
+
Select an item to read
+
+
-` +`; + +const feedListEl = document.getElementById('feed-list')!; +const viewTitleEl = document.getElementById('view-title')!; +const itemListEl = document.getElementById('item-list-container')!; +const itemDetailEl = document.getElementById('item-detail-content')!; + +// --- Rendering Functions --- + +function renderFeeds() { + const { feeds, activeFeedId } = store; + feedListEl.innerHTML = feeds.map((feed: Feed) => + createFeedItem(feed, feed._id === activeFeedId) + ).join(''); +} + +function renderItems() { + const { items, loading } = store; + + if (loading) { + itemListEl.innerHTML = '

Loading items...

'; + return; + } + + if (items.length === 0) { + itemListEl.innerHTML = '

No items found.

'; + return; + } + + itemListEl.innerHTML = ` +
    + ${items.map((item: Item) => ` +
  • +
    ${item.title}
    +
    ${item.feed_title || ''}
    +
  • + `).join('')} +
+ `; + + // Add click listeners to items + itemListEl.querySelectorAll('.item-row').forEach(row => { + row.addEventListener('click', () => { + const id = parseInt(row.getAttribute('data-id') || '0'); + selectItem(id); + }); + }); +} + +async function selectItem(id: number) { + const item = store.items.find((i: Item) => i._id === id); + if (!item) return; + + // Mark active row + itemListEl.querySelectorAll('.item-row').forEach(row => { + row.classList.toggle('active', parseInt(row.getAttribute('data-id') || '0') === id); + }); + + // Render basic detail + itemDetailEl.innerHTML = ` +
+
+

${item.title}

+
+ From ${item.feed_title || 'Unknown'} on ${new Date(item.publish_date).toLocaleString()} +
+
+
+ ${item.description || 'No description available.'} +
+
+ `; + + // Mark as read if not already + if (!item.read) { + try { + await apiFetch(`/api/item/${item._id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ read: true }) + }); + item.read = true; + const row = itemListEl.querySelector(`.item-row[data-id="${id}"]`); + if (row) row.classList.add('read'); + } catch (err) { + console.error('Failed to mark as read', err); + } + } + + // Fetch full content if missing + if (item.url && (!item.full_content || item.full_content === item.description)) { + try { + const res = await apiFetch(`/api/item/${item._id}/content`); + if (res.ok) { + const data = await res.json(); + if (data.full_content) { + item.full_content = data.full_content; + const contentEl = document.getElementById('full-content'); + if (contentEl) contentEl.innerHTML = data.full_content; + } + } + } catch (err) { + console.error('Failed to fetch full content', err); + } + } +} + +// --- Data Actions --- + +async function fetchFeeds() { + try { + const res = await apiFetch('/api/feed/'); + if (!res.ok) throw new Error('Failed to fetch feeds'); + const feeds = await res.json(); + store.setFeeds(feeds); + } catch (err) { + console.error(err); + } +} + +async function fetchItems(feedId?: string, tagName?: string) { + store.setLoading(true); + try { + let url = '/api/stream'; + const params = new URLSearchParams(); + if (feedId) params.append('feed_id', feedId); + if (tagName) params.append('tag', tagName); + + const res = await apiFetch(`${url}?${params.toString()}`); + if (!res.ok) throw new Error('Failed to fetch items'); + const items = await res.json(); + store.setItems(items); + itemDetailEl.innerHTML = '
Select an item to read
'; + } catch (err) { + console.error(err); + store.setItems([]); + } finally { + store.setLoading(false); + } +} + +// --- App Logic --- + +function handleRoute() { + const route = router.getCurrentRoute(); + + if (route.path === '/feed' && route.params.feedId) { + const id = parseInt(route.params.feedId); + store.setActiveFeed(id); + const feed = store.feeds.find((f: Feed) => f._id === id); + viewTitleEl.textContent = feed ? feed.title : `Feed ${id}`; + fetchItems(route.params.feedId); + } else if (route.path === '/tag' && route.params.tagName) { + store.setActiveFeed(null); + viewTitleEl.textContent = `Tag: ${route.params.tagName}`; + fetchItems(undefined, route.params.tagName); + } else { + store.setActiveFeed(null); + viewTitleEl.textContent = 'All Items'; + fetchItems(); + } +} + +// Subscribe to store +store.on('feeds-updated', renderFeeds); +store.on('active-feed-updated', renderFeeds); +store.on('items-updated', renderItems); +store.on('loading-state-changed', renderItems); + +// Subscribe to router +router.addEventListener('route-changed', handleRoute); + +// Global app object for inline handlers +(window as any).app = { + navigate: (path: string) => router.navigate(path) +}; + +// Start +async function init() { + const authRes = await apiFetch('/api/auth'); + if (authRes.status === 401) { + window.location.href = '/login/'; + return; + } + + await fetchFeeds(); + handleRoute(); // handles initial route +} -setupCounter(document.querySelector('#counter')!) +init(); -- cgit v1.2.3