aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.thicket/tickets.jsonl14
-rw-r--r--frontend/src/components/FeedItems.css12
-rw-r--r--frontend/src/components/FeedItems.test.tsx55
-rw-r--r--frontend/src/components/FeedItems.tsx46
4 files changed, 115 insertions, 12 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl
index 10a7ba9..953cce0 100644
--- a/.thicket/tickets.jsonl
+++ b/.thicket/tickets.jsonl
@@ -1,28 +1,32 @@
{"id":"NK-0nf7hu","title":"Implement Frontend Logout","description":"Add logout button/link in dashboard and call /api/logout.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:50:46.760744241Z","updated":"2026-02-13T15:28:14.486180285Z"}
{"id":"NK-0ppv3f","title":"Implement Frontend Settings","description":"Create settings page for managing feeds/categories.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.631640578Z","updated":"2026-02-13T15:04:12.408401691Z"}
{"id":"NK-1phdpf","title":"refactor backend to have a clean API","description":"create a nice clean API for the backend GO code that is more independent of the frontend\n\nensure that it is working with good tests","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T01:52:49.8322638Z","updated":"2026-02-13T04:26:47.517515371Z"}
-{"id":"NK-27or4b","title":"Increase Test Coverage to \u003e80%","description":"Project-wide test coverage is currently ~63%. Key gaps are in the new and packages, as well as some core model logic. Increase coverage to at least 80% to ensure stability.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T05:03:09.677147894Z","updated":"2026-02-13T05:03:09.677147894Z"}
-{"id":"NK-2t5ijy","title":"Mobile Layout Verification for New Frontend","description":"Verify and adjust the new fixed-width layout for mobile devices to ensure responsiveness.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:11:47.456406598Z","updated":"2026-02-13T18:11:47.456406598Z"}
+{"id":"NK-27or4b","title":"Increase Test Coverage to \u003e80%","description":"Project-wide test coverage is currently ~63%. Key gaps are in the new and packages, as well as some core model logic. Increase coverage to at least 80% to ensure stability.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:03:09.677147894Z","updated":"2026-02-13T05:03:09.677147894Z"}
+{"id":"NK-2t5ijy","title":"Mobile Layout Verification for New Frontend","description":"Verify and adjust the new fixed-width layout for mobile devices to ensure responsiveness.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T18:11:47.456406598Z","updated":"2026-02-13T18:11:47.456406598Z"}
{"id":"NK-3om7x2","title":"Implement Feed Items View","description":"Create a component to display items for a selected feed. Fetch items from /api/stream?feed_id=...","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:59:46.161356437Z","updated":"2026-02-13T14:55:14.795643835Z"}
{"id":"NK-59kbij","title":"Implement Frontend Logout","description":"Add logout button to dashboard header. Call /api/logout (need to create this potentially?). Redirect to /login","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:58:18.343464645Z","updated":"2026-02-13T15:01:33.783216589Z"}
+{"id":"NK-5ocxgm","title":"Infinite scroll","description":"a key feature of the original version that when you scroll to the bottom, it catches that and loads more (based on the current filters, etc)","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T19:27:01.14879752Z","updated":"2026-02-13T19:45:02.283640203Z"}
{"id":"NK-6q9nyg","title":"Refactor HTTP-dependent functions for testability","description":"Several functions use http.Get or external libraries directly (GetFullContent uses goose, ResolveFeedURL uses http.Get + goquery, imageProxyHandler uses http.Client). Refactor these to accept interfaces for HTTP fetching so they can be unit tested with mocks. This is the primary blocker for reaching 90% coverage.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:37.630148644Z","updated":"2026-02-13T03:54:37.630148644Z"}
{"id":"NK-7tzbql","title":"Fix TUI Content View Navigation and Interaction","description":"The TUI content view (reading a single item) is currently non-functional or severely limited. Users cannot easily navigate back, scroll, or interact with the content. This task involves improving the 'viewContent' state in the TUI.","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:02:57.382793121Z","updated":"2026-02-13T05:06:15.144485446Z"}
{"id":"NK-9hx0y7","title":"Implement Frontend Login","description":"Create login page and auth logic in the new React frontend. Port functionality from legacy login.html.","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.546395342Z","updated":"2026-02-13T05:50:33.877452063Z"}
+{"id":"NK-ahzf5c","title":"drop \"mark read\" button","description":"there's no mark read/unread buttons, it's just by scrolling!","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-13T19:28:20.708443259Z","updated":"2026-02-13T19:28:20.708443259Z"}
{"id":"NK-bsdwqz","title":"terminal UI","description":"once there is good test coverage and a clean backend API, work on a nice efficient TUI with https://github.com/charmbracelet/bubbletea","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T01:54:02.285738454Z","updated":"2026-02-13T04:42:09.824268427Z"}
-{"id":"NK-ed1iah","title":"Make feed crawling async in API","description":"Currently, POST /api/feed triggers an immediate crawl which blocks the response (or at least keeps the goroutine alive). Refactor the crawling architecture to be truly async with a job queue or status updates, improving API responsiveness and reliability.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.908243985Z","updated":"2026-02-13T04:26:55.908243985Z"}
+{"id":"NK-ed1iah","title":"Make feed crawling async in API","description":"Currently, POST /api/feed triggers an immediate crawl which blocks the response (or at least keeps the goroutine alive). Refactor the crawling architecture to be truly async with a job queue or status updates, improving API responsiveness and reliability.","type":"cleanup","status":"open","priority":4,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.908243985Z","updated":"2026-02-13T04:26:55.908243985Z"}
{"id":"NK-ek0cox","title":"Implement Item Interactions","description":"Add ability to toggle read/unread and star/unstar status for items. Use PUT /item/:id","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:55:14.825454967Z","updated":"2026-02-13T14:58:18.307521003Z"}
{"id":"NK-fnaohu","title":"UI Styling: Dark Mode Support","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:19.59504351Z","updated":"2026-02-13T18:11:46.326064329Z"}
-{"id":"NK-gqkh96","title":"Remaining test coverage gaps","description":"Cross-package test coverage is at 81.2%. The remaining untested functions are: GetFullContent (goose HTTP extraction), indexHandler/serveBoxedFile (rice.MustFindBox), Serve (starts HTTP server), main, util.init. To reach 90%, consider: (1) refactoring GetFullContent to accept an interface for HTTP fetching, (2) refactoring Serve to extract route setup into a testable function, (3) mocking rice.MustFindBox, (4) using feeds from https://trenchant.org/feeds.txt as static test fixtures for integration tests.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:30.298141982Z","updated":"2026-02-13T03:54:30.298141982Z"}
+{"id":"NK-gqkh96","title":"Remaining test coverage gaps","description":"Cross-package test coverage is at 81.2%. The remaining untested functions are: GetFullContent (goose HTTP extraction), indexHandler/serveBoxedFile (rice.MustFindBox), Serve (starts HTTP server), main, util.init. To reach 90%, consider: (1) refactoring GetFullContent to accept an interface for HTTP fetching, (2) refactoring Serve to extract route setup into a testable function, (3) mocking rice.MustFindBox, (4) using feeds from https://trenchant.org/feeds.txt as static test fixtures for integration tests.","type":"cleanup","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T03:54:30.298141982Z","updated":"2026-02-13T03:54:30.298141982Z"}
{"id":"NK-lrew5z","title":"UI Styling: Global Typography \u0026 Layout (Fixed Width)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:04:57.254341682Z","updated":"2026-02-13T18:11:31.436752093Z"}
{"id":"NK-mwf9q2","title":"Implement Tag View","description":"Create frontend view for browsing items by tag/category. Use /tag/:id endpoint.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:04:12.441165286Z","updated":"2026-02-13T18:04:38.644796168Z"}
{"id":"NK-op5594","title":"Ensure 80% Frontend Test Coverage","description":"Configure coverage reporting in vitest and ensure the frontend codebase maintains at least 80% test coverage.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:46:24.13314466Z","updated":"2026-02-13T05:50:46.728239299Z"}
{"id":"NK-qwef98","title":"UI Styling: Controls \u0026 Header","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:18.450759919Z","updated":"2026-02-13T18:11:46.291830432Z"}
{"id":"NK-ric1zs","title":"Migrate frontend to /api/ endpoints","description":"The backend now provides a clean REST API at /api/. Update the frontend UI to use these new endpoints instead of the legacy backward-compatibility routes (/stream/, /feed/, etc.). This will allow for cleaner separation and better utilization of proper REST patterns.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"}
-{"id":"NK-sne5ox","title":"Implement Export/Import UI","description":"Add UI in settings to download OPML export and upload OPML import. Use /export/ and /import/ (need to check if import exists).","type":"","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T15:05:23.266731399Z","updated":"2026-02-13T15:05:23.266731399Z"}
+{"id":"NK-shpyxh","title":"add search to new ui","description":"","type":"epic","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T19:29:44.251257089Z","updated":"2026-02-13T19:29:44.251257089Z"}
+{"id":"NK-sne5ox","title":"Implement Export/Import UI","description":"Add UI in settings to download OPML export and upload OPML import. Use /export/ and /import/ (need to check if import exists).","type":"feature","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T15:05:23.266731399Z","updated":"2026-02-13T15:05:23.266731399Z"}
{"id":"NK-t0nmbj","title":"new web frontend","description":"The current frontend uses an old version of backbone and jquery. Let's \"deprecate\" it -- keep it arouond so we can test against it and use it, but let's be able to also serve and use a nice shiny new frontend written in either simiple, highly efficient vanilla javascript, or put together something in react or similar. Needs to feel fast and low latency!\n\nIt's very important that this new frontend has all the functionality of the existing one AND looks similar (use same style, etc, but adjust a little if needed.)\n\nALSO make it highly testable and have high test coverage as you go. I don't want it to use the Chrome browser plugin thing, just test it on your own using things from the command line you can do.","type":"epic","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T02:01:37.2107893Z","updated":"2026-02-13T05:43:47.613995925Z"}
{"id":"NK-tw0nga","title":"E2E Testing","description":"Set up E2E testing with Playwright or Cypress to verify full flows: Login -\u003e View Feeds -\u003e View Items -\u003e Logout","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:01:33.817314728Z","updated":"2026-02-13T15:46:57.094062908Z"}
{"id":"NK-uy90he","title":"UI Styling: Feed Items (Spacing, Dateline)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:17.289457994Z","updated":"2026-02-13T18:11:46.255816698Z"}
{"id":"NK-x924bu","title":"test coverage","description":"assume the code works properly (it mostly does)\nget to 90% test coverage on the go code","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T01:52:01.042476226Z","updated":"2026-02-13T03:54:21.526519915Z"}
+{"id":"NK-ymf1jb","title":"add \"star\" back in","description":"rather than the word \"star\" it should just have a star that changes colors right next to the title","type":"task","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T19:29:05.582140321Z","updated":"2026-02-13T19:29:05.582140321Z"}
{"id":"NK-zs9we8","title":"UI Styling: Sidebar (Fixed, Gray Background)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:16.188317572Z","updated":"2026-02-13T18:11:46.213993245Z"}
{"id":"NK-zt4e32","title":"Implement Frontend Feed List","description":"Create feed list view in new frontend. Fetch feeds from API.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.58866298Z","updated":"2026-02-13T05:59:46.132148641Z"}
{"id":"NK-d0ghccy","from_ticket_id":"NK-ric1zs","to_ticket_id":"NK-1phdpf","type":"created_from","created":"2026-02-13T04:26:55.875394997Z"}
diff --git a/frontend/src/components/FeedItems.css b/frontend/src/components/FeedItems.css
index f271b34..54c882a 100644
--- a/frontend/src/components/FeedItems.css
+++ b/frontend/src/components/FeedItems.css
@@ -12,10 +12,20 @@
list-style: none;
padding: 0;
}
+
.selected-item-container {
border-left: 4px solid #007bff;
background-color: #f8f9fa;
padding-left: 0.5rem;
- margin-left: -0.5rem; /* Compensate for padding/border to keep alignment */
+ margin-left: -0.5rem;
+ /* Compensate for padding/border to keep alignment */
transition: background-color 0.2s;
}
+
+.loading-more {
+ padding: 2rem;
+ text-align: center;
+ color: #888;
+ font-size: 0.9rem;
+ min-height: 50px;
+} \ No newline at end of file
diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx
index 00118fa..ea68a7c 100644
--- a/frontend/src/components/FeedItems.test.tsx
+++ b/frontend/src/components/FeedItems.test.tsx
@@ -53,8 +53,6 @@ describe('FeedItems Component', () => {
await waitFor(() => {
expect(screen.getByText('Item One')).toBeInTheDocument();
- // Title should now be "Feed Items" based on logic
- expect(screen.getByText('Feed Items')).toBeInTheDocument();
});
const params = new URLSearchParams();
@@ -171,4 +169,57 @@ describe('FeedItems Component', () => {
}));
});
});
+
+ it('loads more items when sentinel becomes visible', async () => {
+ const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }];
+ const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }];
+
+ (global.fetch as any)
+ .mockResolvedValueOnce({ ok: true, json: async () => initialItems })
+ .mockResolvedValueOnce({ ok: true, json: async () => moreItems });
+
+ let observerCallback: IntersectionObserverCallback = () => { };
+ class MockIntersectionObserver {
+ constructor(callback: IntersectionObserverCallback) {
+ observerCallback = callback;
+ }
+ observe = vi.fn();
+ unobserve = vi.fn();
+ disconnect = vi.fn();
+ }
+ window.IntersectionObserver = MockIntersectionObserver as any;
+
+ render(
+ <MemoryRouter>
+ <FeedItems />
+ </MemoryRouter>
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ });
+
+ // Simulate sentinel becoming visible
+ const entry = {
+ isIntersecting: true,
+ target: { id: 'load-more-sentinel' } as unknown as Element,
+ boundingClientRect: {} as DOMRectReadOnly,
+ intersectionRatio: 1,
+ time: 0,
+ rootBounds: null,
+ intersectionRect: {} as DOMRectReadOnly,
+ } as IntersectionObserverEntry;
+
+ act(() => {
+ observerCallback([entry], {} as IntersectionObserver);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Item 0')).toBeInTheDocument();
+ const params = new URLSearchParams();
+ params.append('max_id', '101');
+ params.append('read_filter', 'unread');
+ expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`);
+ });
+ });
});
diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx
index aaec2f4..179d5cd 100644
--- a/frontend/src/components/FeedItems.tsx
+++ b/frontend/src/components/FeedItems.tsx
@@ -11,10 +11,17 @@ export default function FeedItems() {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState('');
- useEffect(() => {
- setLoading(true);
+ const fetchItems = (maxId?: string) => {
+ if (maxId) {
+ setLoadingMore(true);
+ } else {
+ setLoading(true);
+ setItems([]);
+ }
setError('');
let url = '/api/stream';
@@ -26,6 +33,10 @@ export default function FeedItems() {
params.append('tag', tagName);
}
+ if (maxId) {
+ params.append('max_id', maxId);
+ }
+
// Apply filters
if (filterFn === 'all') {
params.append('read_filter', 'all');
@@ -50,13 +61,24 @@ export default function FeedItems() {
return res.json();
})
.then((data) => {
- setItems(data);
+ if (maxId) {
+ setItems((prev) => [...prev, ...data]);
+ } else {
+ setItems(data);
+ }
+ setHasMore(data.length > 0);
setLoading(false);
+ setLoadingMore(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
+ setLoadingMore(false);
});
+ };
+
+ useEffect(() => {
+ fetchItems();
}, [feedId, tagName, filterFn]);
const [selectedIndex, setSelectedIndex] = useState(-1);
@@ -134,6 +156,14 @@ export default function FeedItems() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
+ // Infinity scroll sentinel
+ if (entry.target.id === 'load-more-sentinel') {
+ if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) {
+ fetchItems(String(items[items.length - 1]._id));
+ }
+ return;
+ }
+
// If item is not intersecting and is above the viewport, it's been scrolled past
if (!entry.isIntersecting && entry.boundingClientRect.top < 0) {
const index = Number(entry.target.getAttribute('data-index'));
@@ -154,8 +184,11 @@ export default function FeedItems() {
if (el) observer.observe(el);
});
+ const sentinel = document.getElementById('load-more-sentinel');
+ if (sentinel) observer.observe(sentinel);
+
return () => observer.disconnect();
- }, [items]);
+ }, [items, loadingMore, hasMore]);
if (loading) return <div className="feed-items-loading">Loading items...</div>;
if (error) return <div className="feed-items-error">Error: {error}</div>;
@@ -178,6 +211,11 @@ export default function FeedItems() {
<FeedItem item={item} />
</div>
))}
+ {hasMore && (
+ <div id="load-more-sentinel" className="loading-more">
+ {loadingMore ? 'Loading more...' : ''}
+ </div>
+ )}
</ul>
)}
</div>