diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 06:55:21 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 06:55:21 -0800 |
| commit | 3ba71500bc2d60a00ca81b9439305029670f4d52 (patch) | |
| tree | 5752a119effd739c62c80c2d15d4520c2e53eadf | |
| parent | 2c3cad528a247c771bca136466337877f76f280f (diff) | |
| download | neko-3ba71500bc2d60a00ca81b9439305029670f4d52.tar.gz neko-3ba71500bc2d60a00ca81b9439305029670f4d52.tar.bz2 neko-3ba71500bc2d60a00ca81b9439305029670f4d52.zip | |
Implement Frontend Feed Items View with tests
21 files changed, 912 insertions, 77 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index e3174e1..8af7e2b 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -2,12 +2,13 @@ {"id":"NK-0ppv3f","title":"Implement Frontend Settings","description":"Create settings page for managing feeds/categories.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.631640578Z","updated":"2026-02-13T05:44:01.631640578Z"} {"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-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":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:59:46.161356437Z","updated":"2026-02-13T05:59:46.161356437Z"} +{"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-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-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-ek0cox","title":"Implement Item Interactions","description":"Add ability to toggle read/unread and star/unstar status for items. Use PUT /item/:id","type":"","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:55:14.825454967Z","updated":"2026-02-13T14:55:14.825454967Z"} {"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-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-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"} @@ -24,4 +25,5 @@ {"id":"NK-dlvmiyc","from_ticket_id":"NK-7tzbql","to_ticket_id":"NK-bsdwqz","type":"created_from","created":"2026-02-13T05:02:57.392616851Z"} {"id":"NK-dm75oc8","from_ticket_id":"NK-3om7x2","to_ticket_id":"NK-zt4e32","type":"created_from","created":"2026-02-13T05:59:46.169842933Z"} {"id":"NK-dofihuz","from_ticket_id":"NK-0ppv3f","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.640770816Z"} +{"id":"NK-dw8luqe","from_ticket_id":"NK-ek0cox","to_ticket_id":"NK-3om7x2","type":"created_from","created":"2026-02-13T14:55:14.832352853Z"} {"id":"NK-dwav3hh","from_ticket_id":"NK-6q9nyg","to_ticket_id":"NK-x924bu","type":"created_from","created":"2026-02-13T03:54:37.639569082Z"} diff --git a/frontend/coverage/clover.xml b/frontend/coverage/clover.xml index 5e830d2..8251808 100644 --- a/frontend/coverage/clover.xml +++ b/frontend/coverage/clover.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> -<coverage generated="1770962373767" clover="3.2.0"> - <project timestamp="1770962373767" name="All files"> - <metrics statements="48" coveredstatements="44" conditionals="24" coveredconditionals="19" methods="15" coveredmethods="14" elements="87" coveredelements="77" complexity="0" loc="48" ncloc="48" packages="2" files="6" classes="6"/> +<coverage generated="1770962571342" clover="3.2.0"> + <project timestamp="1770962571342" name="All files"> + <metrics statements="68" coveredstatements="61" conditionals="42" coveredconditionals="30" methods="21" coveredmethods="19" elements="131" coveredelements="110" complexity="0" loc="68" ncloc="68" packages="2" files="8" classes="8"/> <package name="src"> <metrics statements="15" coveredstatements="12" conditionals="6" coveredconditionals="4" methods="6" coveredmethods="5"/> <file name="App.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.css"> @@ -22,33 +22,59 @@ <line num="27" count="1" type="cond" truecount="1" falsecount="1"/> <line num="28" count="0" type="stmt"/> <line num="31" count="1" type="stmt"/> - <line num="37" count="1" type="stmt"/> - <line num="58" count="2" type="stmt"/> + <line num="38" count="1" type="stmt"/> + <line num="62" count="2" type="stmt"/> </file> </package> <package name="src.components"> - <metrics statements="33" coveredstatements="32" conditionals="18" coveredconditionals="15" methods="9" coveredmethods="9"/> + <metrics statements="53" coveredstatements="49" conditionals="36" coveredconditionals="26" methods="15" coveredmethods="14"/> + <file name="FeedItems.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css"> + <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> + </file> + <file name="FeedItems.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx"> + <metrics statements="20" coveredstatements="17" conditionals="18" coveredconditionals="11" methods="6" coveredmethods="5"/> + <line num="7" count="3" type="stmt"/> + <line num="8" count="3" type="stmt"/> + <line num="9" count="3" type="stmt"/> + <line num="10" count="3" type="stmt"/> + <line num="12" count="3" type="stmt"/> + <line num="13" count="2" type="stmt"/> + <line num="14" count="2" type="stmt"/> + <line num="16" count="2" type="cond" truecount="1" falsecount="1"/> + <line num="20" count="2" type="stmt"/> + <line num="22" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="23" count="0" type="stmt"/> + <line num="25" count="1" type="stmt"/> + <line num="28" count="1" type="stmt"/> + <line num="29" count="1" type="stmt"/> + <line num="32" count="0" type="stmt"/> + <line num="33" count="0" type="stmt"/> + <line num="37" count="3" type="cond" truecount="2" falsecount="0"/> + <line num="38" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="40" count="1" type="stmt"/> + <line num="49" count="2" type="cond" truecount="2" falsecount="0"/> + </file> <file name="FeedList.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css"> <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> </file> <file name="FeedList.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx"> <metrics statements="16" coveredstatements="15" conditionals="12" coveredconditionals="10" methods="6" coveredmethods="6"/> - <line num="6" count="9" type="stmt"/> - <line num="7" count="9" type="stmt"/> - <line num="8" count="9" type="stmt"/> - <line num="10" count="9" type="stmt"/> - <line num="11" count="5" type="stmt"/> - <line num="13" count="3" type="cond" truecount="1" falsecount="1"/> - <line num="14" count="0" type="stmt"/> - <line num="16" count="3" type="stmt"/> - <line num="19" count="3" type="stmt"/> + <line num="7" count="8" type="stmt"/> + <line num="8" count="8" type="stmt"/> + <line num="9" count="8" type="stmt"/> + <line num="11" count="8" type="stmt"/> + <line num="12" count="5" type="stmt"/> + <line num="14" count="3" type="cond" truecount="1" falsecount="1"/> + <line num="15" count="0" type="stmt"/> + <line num="17" count="3" type="stmt"/> <line num="20" count="3" type="stmt"/> - <line num="23" count="1" type="stmt"/> + <line num="21" count="3" type="stmt"/> <line num="24" count="1" type="stmt"/> - <line num="28" count="9" type="cond" truecount="2" falsecount="0"/> - <line num="29" count="4" type="cond" truecount="2" falsecount="0"/> - <line num="31" count="3" type="stmt"/> - <line num="39" count="2" type="stmt"/> + <line num="25" count="1" type="stmt"/> + <line num="29" count="8" type="cond" truecount="2" falsecount="0"/> + <line num="30" count="3" type="cond" truecount="2" falsecount="0"/> + <line num="32" count="2" type="stmt"/> + <line num="40" count="2" type="stmt"/> </file> <file name="Login.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css"> <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json index 1fe79ba..8fc3ade 100644 --- a/frontend/coverage/coverage-final.json +++ b/frontend/coverage/coverage-final.json @@ -1,7 +1,9 @@ {"/Users/adam/workspace/vibecode/neko/frontend/src/App.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx","statementMap":{"0":{"start":{"line":8,"column":22},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":8},"end":{"line":9,"column":null}},"2":{"start":{"line":11,"column":2},"end":{"line":21,"column":null}},"3":{"start":{"line":12,"column":4},"end":{"line":20,"column":null}},"4":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"5":{"start":{"line":15,"column":10},"end":{"line":15,"column":null}},"6":{"start":{"line":17,"column":10},"end":{"line":17,"column":null}},"7":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"8":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"12":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"13":{"start":{"line":37,"column":2},"end":{"line":53,"column":null}},"14":{"start":{"line":58,"column":2},"end":{"line":71,"column":null}}},"fnMap":{"0":{"name":"RequireAuth","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":21}},"loc":{"start":{"line":7,"column":69},"end":{"line":32,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":12},"end":{"line":11,"column":18}},"loc":{"start":{"line":11,"column":18},"end":{"line":21,"column":5}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":12},"end":{"line":13,"column":13}},"loc":{"start":{"line":13,"column":21},"end":{"line":19,"column":7}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":13},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"line":20},"4":{"name":"Dashboard","decl":{"start":{"line":36,"column":9},"end":{"line":36,"column":21}},"loc":{"start":{"line":36,"column":21},"end":{"line":55,"column":null}},"line":36},"5":{"name":"App","decl":{"start":{"line":57,"column":9},"end":{"line":57,"column":15}},"loc":{"start":{"line":57,"column":15},"end":{"line":73,"column":null}},"line":57}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},{"start":{"line":16,"column":15},"end":{"line":18,"column":null}}],"line":14},"1":{"loc":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":27}},"s":{"0":2,"1":2,"2":2,"3":1,"4":1,"5":1,"6":0,"7":0,"8":2,"9":1,"10":1,"11":0,"12":1,"13":1,"14":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":6,"lastStatement":15,"seen":{"f:7:9:7:21":0,"s:8:22:8:Infinity":0,"s:9:8:9:Infinity":1,"s:11:2:21:Infinity":2,"f:11:12:11:18":1,"s:12:4:20:Infinity":3,"f:13:12:13:13":2,"b:14:8:18:Infinity:16:15:18:Infinity":0,"s:14:8:18:Infinity":4,"s:15:10:15:Infinity":5,"s:17:10:17:Infinity":6,"f:20:13:20:19":3,"s:20:19:20:33":7,"b:23:2:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:2:25:Infinity":8,"s:24:4:24:Infinity":9,"b:27:2:29:Infinity:undefined:undefined:undefined:undefined":2,"s:27:2:29:Infinity":10,"s:28:4:28:Infinity":11,"s:31:2:31:Infinity":12,"f:36:9:36:21":4,"s:37:2:53:Infinity":13,"f:57:9:57:15":5,"s:58:2:71:Infinity":14}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx","statementMap":{"0":{"start":{"line":8,"column":22},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":8},"end":{"line":9,"column":null}},"2":{"start":{"line":11,"column":2},"end":{"line":21,"column":null}},"3":{"start":{"line":12,"column":4},"end":{"line":20,"column":null}},"4":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"5":{"start":{"line":15,"column":10},"end":{"line":15,"column":null}},"6":{"start":{"line":17,"column":10},"end":{"line":17,"column":null}},"7":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"8":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"12":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"13":{"start":{"line":38,"column":2},"end":{"line":57,"column":null}},"14":{"start":{"line":62,"column":2},"end":{"line":75,"column":null}}},"fnMap":{"0":{"name":"RequireAuth","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":21}},"loc":{"start":{"line":7,"column":69},"end":{"line":32,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":12},"end":{"line":11,"column":18}},"loc":{"start":{"line":11,"column":18},"end":{"line":21,"column":5}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":12},"end":{"line":13,"column":13}},"loc":{"start":{"line":13,"column":21},"end":{"line":19,"column":7}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":13},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"line":20},"4":{"name":"Dashboard","decl":{"start":{"line":37,"column":9},"end":{"line":37,"column":21}},"loc":{"start":{"line":37,"column":21},"end":{"line":59,"column":null}},"line":37},"5":{"name":"App","decl":{"start":{"line":61,"column":9},"end":{"line":61,"column":15}},"loc":{"start":{"line":61,"column":15},"end":{"line":77,"column":null}},"line":61}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},{"start":{"line":16,"column":15},"end":{"line":18,"column":null}}],"line":14},"1":{"loc":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":27}},"s":{"0":2,"1":2,"2":2,"3":1,"4":1,"5":1,"6":0,"7":0,"8":2,"9":1,"10":1,"11":0,"12":1,"13":1,"14":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":6,"lastStatement":15,"seen":{"f:7:9:7:21":0,"s:8:22:8:Infinity":0,"s:9:8:9:Infinity":1,"s:11:2:21:Infinity":2,"f:11:12:11:18":1,"s:12:4:20:Infinity":3,"f:13:12:13:13":2,"b:14:8:18:Infinity:16:15:18:Infinity":0,"s:14:8:18:Infinity":4,"s:15:10:15:Infinity":5,"s:17:10:17:Infinity":6,"f:20:13:20:19":3,"s:20:19:20:33":7,"b:23:2:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:2:25:Infinity":8,"s:24:4:24:Infinity":9,"b:27:2:29:Infinity:undefined:undefined:undefined:undefined":2,"s:27:2:29:Infinity":10,"s:28:4:28:Infinity":11,"s:31:2:31:Infinity":12,"f:37:9:37:21":4,"s:38:2:57:Infinity":13,"f:61:9:61:15":5,"s:62:2:75:Infinity":14}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx","statementMap":{"0":{"start":{"line":7,"column":19},"end":{"line":7,"column":null}},"1":{"start":{"line":8,"column":26},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":30},"end":{"line":9,"column":null}},"3":{"start":{"line":10,"column":26},"end":{"line":10,"column":null}},"4":{"start":{"line":12,"column":4},"end":{"line":35,"column":null}},"5":{"start":{"line":13,"column":8},"end":{"line":13,"column":null}},"6":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"7":{"start":{"line":16,"column":20},"end":{"line":18,"column":null}},"8":{"start":{"line":20,"column":8},"end":{"line":34,"column":null}},"9":{"start":{"line":22,"column":16},"end":{"line":24,"column":null}},"10":{"start":{"line":23,"column":20},"end":{"line":23,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"13":{"start":{"line":29,"column":16},"end":{"line":29,"column":null}},"14":{"start":{"line":32,"column":16},"end":{"line":32,"column":null}},"15":{"start":{"line":33,"column":16},"end":{"line":33,"column":null}},"16":{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},"17":{"start":{"line":37,"column":17},"end":{"line":37,"column":null}},"18":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"19":{"start":{"line":38,"column":15},"end":{"line":38,"column":null}},"20":{"start":{"line":40,"column":4},"end":{"line":64,"column":null}},"21":{"start":{"line":49,"column":24},"end":{"line":60,"column":null}}},"fnMap":{"0":{"name":"FeedItems","decl":{"start":{"line":6,"column":24},"end":{"line":6,"column":36}},"loc":{"start":{"line":6,"column":36},"end":{"line":66,"column":null}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":12,"column":14},"end":{"line":12,"column":20}},"loc":{"start":{"line":12,"column":20},"end":{"line":35,"column":7}},"line":12},"2":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":18},"end":{"line":21,"column":19}},"loc":{"start":{"line":21,"column":27},"end":{"line":26,"column":13}},"line":21},"3":{"name":"(anonymous_3)","decl":{"start":{"line":27,"column":18},"end":{"line":27,"column":19}},"loc":{"start":{"line":27,"column":28},"end":{"line":30,"column":13}},"line":27},"4":{"name":"(anonymous_4)","decl":{"start":{"line":31,"column":19},"end":{"line":31,"column":20}},"loc":{"start":{"line":31,"column":28},"end":{"line":34,"column":13}},"line":31},"5":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":31},"end":{"line":48,"column":32}},"loc":{"start":{"line":49,"column":24},"end":{"line":60,"column":null}},"line":49}},"branchMap":{"0":{"loc":{"start":{"line":16,"column":20},"end":{"line":18,"column":null}},"type":"cond-expr","locations":[{"start":{"line":17,"column":14},"end":{"line":17,"column":null}},{"start":{"line":18,"column":14},"end":{"line":18,"column":null}}],"line":16},"1":{"loc":{"start":{"line":22,"column":16},"end":{"line":24,"column":null}},"type":"if","locations":[{"start":{"line":22,"column":16},"end":{"line":24,"column":null}},{"start":{},"end":{}}],"line":22},"2":{"loc":{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},"type":"if","locations":[{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},{"start":{},"end":{}}],"line":37},"3":{"loc":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"type":"if","locations":[{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},{"start":{},"end":{}}],"line":38},"4":{"loc":{"start":{"line":44,"column":13},"end":{"line":62,"column":null}},"type":"cond-expr","locations":[{"start":{"line":45,"column":16},"end":{"line":45,"column":null}},{"start":{"line":47,"column":16},"end":{"line":62,"column":null}}],"line":44},"5":{"loc":{"start":{"line":49,"column":62},"end":{"line":49,"column":91}},"type":"cond-expr","locations":[{"start":{"line":49,"column":74},"end":{"line":49,"column":83}},{"start":{"line":49,"column":83},"end":{"line":49,"column":91}}],"line":49},"6":{"loc":{"start":{"line":51,"column":33},"end":{"line":51,"column":null}},"type":"binary-expr","locations":[{"start":{"line":51,"column":33},"end":{"line":51,"column":47}},{"start":{"line":51,"column":47},"end":{"line":51,"column":null}}],"line":51},"7":{"loc":{"start":{"line":55,"column":33},"end":{"line":55,"column":null}},"type":"binary-expr","locations":[{"start":{"line":55,"column":33},"end":{"line":55,"column":52}},{"start":{"line":55,"column":52},"end":{"line":55,"column":null}}],"line":55},"8":{"loc":{"start":{"line":57,"column":29},"end":{"line":58,"column":null}},"type":"binary-expr","locations":[{"start":{"line":57,"column":29},"end":{"line":57,"column":null}},{"start":{"line":58,"column":32},"end":{"line":58,"column":null}}],"line":57}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":2,"6":2,"7":2,"8":2,"9":1,"10":0,"11":1,"12":1,"13":1,"14":0,"15":0,"16":3,"17":2,"18":1,"19":1,"20":1,"21":2},"f":{"0":3,"1":2,"2":1,"3":1,"4":0,"5":2},"b":{"0":[2,0],"1":[0,1],"2":[2,1],"3":[0,1],"4":[0,1],"5":[1,1],"6":[2,0],"7":[2,0],"8":[2,0]},"meta":{"lastBranch":9,"lastFunction":6,"lastStatement":22,"seen":{"f:6:24:6:36":0,"s:7:19:7:Infinity":0,"s:8:26:8:Infinity":1,"s:9:30:9:Infinity":2,"s:10:26:10:Infinity":3,"s:12:4:35:Infinity":4,"f:12:14:12:20":1,"s:13:8:13:Infinity":5,"s:14:8:14:Infinity":6,"s:16:20:18:Infinity":7,"b:17:14:17:Infinity:18:14:18:Infinity":0,"s:20:8:34:Infinity":8,"f:21:18:21:19":2,"b:22:16:24:Infinity:undefined:undefined:undefined:undefined":1,"s:22:16:24:Infinity":9,"s:23:20:23:Infinity":10,"s:25:16:25:Infinity":11,"f:27:18:27:19":3,"s:28:16:28:Infinity":12,"s:29:16:29:Infinity":13,"f:31:19:31:20":4,"s:32:16:32:Infinity":14,"s:33:16:33:Infinity":15,"b:37:4:37:Infinity:undefined:undefined:undefined:undefined":2,"s:37:4:37:Infinity":16,"s:37:17:37:Infinity":17,"b:38:4:38:Infinity:undefined:undefined:undefined:undefined":3,"s:38:4:38:Infinity":18,"s:38:15:38:Infinity":19,"s:40:4:64:Infinity":20,"b:45:16:45:Infinity:47:16:62:Infinity":4,"f:48:31:48:32":5,"s:49:24:60:Infinity":21,"b:49:74:49:83:49:83:49:91":5,"b:51:33:51:47:51:47:51:Infinity":6,"b:55:33:55:52:55:52:55:Infinity":7,"b:57:29:57:Infinity:58:32:58:Infinity":8}}} ,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx","statementMap":{"0":{"start":{"line":6,"column":26},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":30},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":26},"end":{"line":8,"column":null}},"3":{"start":{"line":10,"column":4},"end":{"line":26,"column":null}},"4":{"start":{"line":11,"column":8},"end":{"line":25,"column":null}},"5":{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},"6":{"start":{"line":14,"column":20},"end":{"line":14,"column":null}},"7":{"start":{"line":16,"column":16},"end":{"line":16,"column":null}},"8":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"9":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"10":{"start":{"line":23,"column":16},"end":{"line":23,"column":null}},"11":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"12":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"13":{"start":{"line":28,"column":17},"end":{"line":28,"column":null}},"14":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"15":{"start":{"line":29,"column":15},"end":{"line":29,"column":null}},"16":{"start":{"line":31,"column":4},"end":{"line":48,"column":null}},"17":{"start":{"line":39,"column":24},"end":{"line":44,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":35}},"loc":{"start":{"line":5,"column":35},"end":{"line":50,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":14},"end":{"line":10,"column":20}},"loc":{"start":{"line":10,"column":20},"end":{"line":26,"column":7}},"line":10},"2":{"name":"(anonymous_2)","decl":{"start":{"line":12,"column":18},"end":{"line":12,"column":19}},"loc":{"start":{"line":12,"column":27},"end":{"line":17,"column":13}},"line":12},"3":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":18},"end":{"line":18,"column":19}},"loc":{"start":{"line":18,"column":28},"end":{"line":21,"column":13}},"line":18},"4":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":19},"end":{"line":22,"column":20}},"loc":{"start":{"line":22,"column":28},"end":{"line":25,"column":13}},"line":22},"5":{"name":"(anonymous_5)","decl":{"start":{"line":38,"column":31},"end":{"line":38,"column":32}},"loc":{"start":{"line":39,"column":24},"end":{"line":44,"column":null}},"line":39}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},"type":"if","locations":[{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"type":"if","locations":[{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},{"start":{},"end":{}}],"line":28},"2":{"loc":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":29},"3":{"loc":{"start":{"line":34,"column":13},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":16},"end":{"line":35,"column":null}},{"start":{"line":37,"column":16},"end":{"line":46,"column":null}}],"line":34},"4":{"loc":{"start":{"line":41,"column":33},"end":{"line":41,"column":null}},"type":"binary-expr","locations":[{"start":{"line":41,"column":33},"end":{"line":41,"column":47}},{"start":{"line":41,"column":47},"end":{"line":41,"column":null}}],"line":41},"5":{"loc":{"start":{"line":43,"column":29},"end":{"line":43,"column":null}},"type":"binary-expr","locations":[{"start":{"line":43,"column":29},"end":{"line":43,"column":46}},{"start":{"line":43,"column":46},"end":{"line":43,"column":null}}],"line":43}},"s":{"0":9,"1":9,"2":9,"3":9,"4":5,"5":3,"6":0,"7":3,"8":3,"9":3,"10":1,"11":1,"12":9,"13":5,"14":4,"15":4,"16":3,"17":2},"f":{"0":9,"1":5,"2":3,"3":3,"4":1,"5":2},"b":{"0":[0,3],"1":[5,4],"2":[1,3],"3":[2,1],"4":[2,0],"5":[2,2]},"meta":{"lastBranch":6,"lastFunction":6,"lastStatement":18,"seen":{"f:5:24:5:35":0,"s:6:26:6:Infinity":0,"s:7:30:7:Infinity":1,"s:8:26:8:Infinity":2,"s:10:4:26:Infinity":3,"f:10:14:10:20":1,"s:11:8:25:Infinity":4,"f:12:18:12:19":2,"b:13:16:15:Infinity:undefined:undefined:undefined:undefined":0,"s:13:16:15:Infinity":5,"s:14:20:14:Infinity":6,"s:16:16:16:Infinity":7,"f:18:18:18:19":3,"s:19:16:19:Infinity":8,"s:20:16:20:Infinity":9,"f:22:19:22:20":4,"s:23:16:23:Infinity":10,"s:24:16:24:Infinity":11,"b:28:4:28:Infinity:undefined:undefined:undefined:undefined":1,"s:28:4:28:Infinity":12,"s:28:17:28:Infinity":13,"b:29:4:29:Infinity:undefined:undefined:undefined:undefined":2,"s:29:4:29:Infinity":14,"s:29:15:29:Infinity":15,"s:31:4:48:Infinity":16,"b:35:16:35:Infinity:37:16:46:Infinity":3,"f:38:31:38:32":5,"s:39:24:44:Infinity":17,"b:41:33:41:47:41:47:41:Infinity":4,"b:43:29:43:46:43:46:43:Infinity":5}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx","statementMap":{"0":{"start":{"line":7,"column":26},"end":{"line":7,"column":null}},"1":{"start":{"line":8,"column":30},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"3":{"start":{"line":11,"column":4},"end":{"line":27,"column":null}},"4":{"start":{"line":12,"column":8},"end":{"line":26,"column":null}},"5":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"6":{"start":{"line":15,"column":20},"end":{"line":15,"column":null}},"7":{"start":{"line":17,"column":16},"end":{"line":17,"column":null}},"8":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"9":{"start":{"line":21,"column":16},"end":{"line":21,"column":null}},"10":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"13":{"start":{"line":29,"column":17},"end":{"line":29,"column":null}},"14":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"15":{"start":{"line":30,"column":15},"end":{"line":30,"column":null}},"16":{"start":{"line":32,"column":4},"end":{"line":49,"column":null}},"17":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":6,"column":24},"end":{"line":6,"column":35}},"loc":{"start":{"line":6,"column":35},"end":{"line":51,"column":null}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":20}},"loc":{"start":{"line":11,"column":20},"end":{"line":27,"column":7}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":18},"end":{"line":13,"column":19}},"loc":{"start":{"line":13,"column":27},"end":{"line":18,"column":13}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":19,"column":18},"end":{"line":19,"column":19}},"loc":{"start":{"line":19,"column":28},"end":{"line":22,"column":13}},"line":19},"4":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":19},"end":{"line":23,"column":20}},"loc":{"start":{"line":23,"column":28},"end":{"line":26,"column":13}},"line":23},"5":{"name":"(anonymous_5)","decl":{"start":{"line":39,"column":31},"end":{"line":39,"column":32}},"loc":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}},"line":40}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},{"start":{},"end":{}}],"line":14},"1":{"loc":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":29},"2":{"loc":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"type":"if","locations":[{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},{"start":{},"end":{}}],"line":30},"3":{"loc":{"start":{"line":35,"column":13},"end":{"line":47,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":16},"end":{"line":36,"column":null}},{"start":{"line":38,"column":16},"end":{"line":47,"column":null}}],"line":35},"4":{"loc":{"start":{"line":42,"column":33},"end":{"line":42,"column":null}},"type":"binary-expr","locations":[{"start":{"line":42,"column":33},"end":{"line":42,"column":47}},{"start":{"line":42,"column":47},"end":{"line":42,"column":null}}],"line":42},"5":{"loc":{"start":{"line":44,"column":29},"end":{"line":44,"column":null}},"type":"binary-expr","locations":[{"start":{"line":44,"column":29},"end":{"line":44,"column":46}},{"start":{"line":44,"column":46},"end":{"line":44,"column":null}}],"line":44}},"s":{"0":8,"1":8,"2":8,"3":8,"4":5,"5":3,"6":0,"7":3,"8":3,"9":3,"10":1,"11":1,"12":8,"13":5,"14":3,"15":3,"16":2,"17":2},"f":{"0":8,"1":5,"2":3,"3":3,"4":1,"5":2},"b":{"0":[0,3],"1":[5,3],"2":[1,2],"3":[1,1],"4":[2,0],"5":[2,2]},"meta":{"lastBranch":6,"lastFunction":6,"lastStatement":18,"seen":{"f:6:24:6:35":0,"s:7:26:7:Infinity":0,"s:8:30:8:Infinity":1,"s:9:26:9:Infinity":2,"s:11:4:27:Infinity":3,"f:11:14:11:20":1,"s:12:8:26:Infinity":4,"f:13:18:13:19":2,"b:14:16:16:Infinity:undefined:undefined:undefined:undefined":0,"s:14:16:16:Infinity":5,"s:15:20:15:Infinity":6,"s:17:16:17:Infinity":7,"f:19:18:19:19":3,"s:20:16:20:Infinity":8,"s:21:16:21:Infinity":9,"f:23:19:23:20":4,"s:24:16:24:Infinity":10,"s:25:16:25:Infinity":11,"b:29:4:29:Infinity:undefined:undefined:undefined:undefined":1,"s:29:4:29:Infinity":12,"s:29:17:29:Infinity":13,"b:30:4:30:Infinity:undefined:undefined:undefined:undefined":2,"s:30:4:30:Infinity":14,"s:30:15:30:Infinity":15,"s:32:4:49:Infinity":16,"b:36:16:36:Infinity:38:16:47:Infinity":3,"f:39:31:39:32":5,"s:40:24:45:Infinity":17,"b:42:33:42:47:42:47:42:Infinity":4,"b:44:29:44:46:44:46:44:Infinity":5}}} ,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} ,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx","statementMap":{"0":{"start":{"line":6,"column":32},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":26},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":10},"end":{"line":8,"column":null}},"3":{"start":{"line":10,"column":25},"end":{"line":33,"column":null}},"4":{"start":{"line":11,"column":8},"end":{"line":11,"column":null}},"5":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"6":{"start":{"line":14,"column":8},"end":{"line":32,"column":null}},"7":{"start":{"line":16,"column":27},"end":{"line":16,"column":null}},"8":{"start":{"line":17,"column":12},"end":{"line":17,"column":null}},"9":{"start":{"line":19,"column":24},"end":{"line":22,"column":null}},"10":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":27,"column":29},"end":{"line":27,"column":null}},"13":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"14":{"start":{"line":31,"column":12},"end":{"line":31,"column":null}},"15":{"start":{"line":35,"column":4},"end":{"line":52,"column":null}},"16":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"Login","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":32}},"loc":{"start":{"line":5,"column":32},"end":{"line":54,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":25},"end":{"line":10,"column":32}},"loc":{"start":{"line":10,"column":49},"end":{"line":33,"column":null}},"line":10},"2":{"name":"(anonymous_2)","decl":{"start":{"line":45,"column":34},"end":{"line":45,"column":35}},"loc":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}},"line":45}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},{"start":{"line":26,"column":19},"end":{"line":29,"column":null}}],"line":24},"1":{"loc":{"start":{"line":28,"column":25},"end":{"line":28,"column":55}},"type":"binary-expr","locations":[{"start":{"line":28,"column":25},"end":{"line":28,"column":41}},{"start":{"line":28,"column":41},"end":{"line":28,"column":55}}],"line":28},"2":{"loc":{"start":{"line":49,"column":17},"end":{"line":49,"column":null}},"type":"binary-expr","locations":[{"start":{"line":49,"column":17},"end":{"line":49,"column":26}},{"start":{"line":49,"column":26},"end":{"line":49,"column":null}}],"line":49}},"s":{"0":14,"1":14,"2":14,"3":14,"4":3,"5":3,"6":3,"7":3,"8":3,"9":3,"10":2,"11":1,"12":1,"13":1,"14":1,"15":14,"16":3},"f":{"0":14,"1":3,"2":3},"b":{"0":[1,1],"1":[1,0],"2":[14,2]},"meta":{"lastBranch":3,"lastFunction":3,"lastStatement":17,"seen":{"f:5:24:5:32":0,"s:6:32:6:Infinity":0,"s:7:26:7:Infinity":1,"s:8:10:8:Infinity":2,"s:10:25:33:Infinity":3,"f:10:25:10:32":1,"s:11:8:11:Infinity":4,"s:12:8:12:Infinity":5,"s:14:8:32:Infinity":6,"s:16:27:16:Infinity":7,"s:17:12:17:Infinity":8,"s:19:24:22:Infinity":9,"b:24:12:29:Infinity:26:19:29:Infinity":0,"s:24:12:29:Infinity":10,"s:25:16:25:Infinity":11,"s:27:29:27:Infinity":12,"s:28:16:28:Infinity":13,"b:28:25:28:41:28:41:28:55":1,"s:31:12:31:Infinity":14,"s:35:4:52:Infinity":15,"f:45:34:45:35":2,"s:45:41:45:Infinity":16,"b:49:17:49:26:49:26:49:Infinity":2}}} } diff --git a/frontend/coverage/index.html b/frontend/coverage/index.html index e90ae39..8011000 100644 --- a/frontend/coverage/index.html +++ b/frontend/coverage/index.html @@ -23,30 +23,30 @@ <div class='clearfix'> <div class='fl pad1y space-right2'> - <span class="strong">92% </span> + <span class="strong">90.27% </span> <span class="quiet">Statements</span> - <span class='fraction'>46/50</span> + <span class='fraction'>65/72</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">79.16% </span> + <span class="strong">71.42% </span> <span class="quiet">Branches</span> - <span class='fraction'>19/24</span> + <span class='fraction'>30/42</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">93.33% </span> + <span class="strong">90.47% </span> <span class="quiet">Functions</span> - <span class='fraction'>14/15</span> + <span class='fraction'>19/21</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">91.66% </span> + <span class="strong">89.7% </span> <span class="quiet">Lines</span> - <span class='fraction'>44/48</span> + <span class='fraction'>61/68</span> </div> @@ -95,17 +95,17 @@ <tr> <td class="file high" data-value="src/components"><a href="src/components/index.html">src/components</a></td> - <td data-value="97.14" class="pic high"> - <div class="chart"><div class="cover-fill" style="width: 97%"></div><div class="cover-empty" style="width: 3%"></div></div> + <td data-value="92.98" class="pic high"> + <div class="chart"><div class="cover-fill" style="width: 92%"></div><div class="cover-empty" style="width: 8%"></div></div> </td> - <td data-value="97.14" class="pct high">97.14%</td> - <td data-value="35" class="abs high">34/35</td> - <td data-value="83.33" class="pct high">83.33%</td> - <td data-value="18" class="abs high">15/18</td> - <td data-value="100" class="pct high">100%</td> - <td data-value="9" class="abs high">9/9</td> - <td data-value="96.96" class="pct high">96.96%</td> - <td data-value="33" class="abs high">32/33</td> + <td data-value="92.98" class="pct high">92.98%</td> + <td data-value="57" class="abs high">53/57</td> + <td data-value="72.22" class="pct medium">72.22%</td> + <td data-value="36" class="abs medium">26/36</td> + <td data-value="93.33" class="pct high">93.33%</td> + <td data-value="15" class="abs high">14/15</td> + <td data-value="92.45" class="pct high">92.45%</td> + <td data-value="53" class="abs high">49/53</td> </tr> </tbody> @@ -116,7 +116,7 @@ <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="prettify.js"></script> <script> diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html index 4ba93ef..21cb0dc 100644 --- a/frontend/coverage/src/App.css.html +++ b/frontend/coverage/src/App.css.html @@ -247,7 +247,7 @@ body { <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/coverage/src/App.tsx.html b/frontend/coverage/src/App.tsx.html index 9d8fd6f..a233a8d 100644 --- a/frontend/coverage/src/App.tsx.html +++ b/frontend/coverage/src/App.tsx.html @@ -138,7 +138,11 @@ <a name='L73'></a><a href='#L73'>73</a> <a name='L74'></a><a href='#L74'>74</a> <a name='L75'></a><a href='#L75'>75</a> -<a name='L76'></a><a href='#L76'>76</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<a name='L76'></a><a href='#L76'>76</a> +<a name='L77'></a><a href='#L77'>77</a> +<a name='L78'></a><a href='#L78'>78</a> +<a name='L79'></a><a href='#L79'>79</a> +<a name='L80'></a><a href='#L80'>80</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -174,6 +178,7 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">1x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -195,6 +200,9 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">2x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -247,6 +255,7 @@ function RequireAuth({ children }: { children: React.ReactElement }) { } import FeedList from './components/FeedList'; +import FeedItems from './components/FeedItems'; function Dashboard() { return ( @@ -262,7 +271,10 @@ function Dashboard() { <FeedList /> </aside> <main className="dashboard-main"> - <p>Select a feed to view items.</p> + <Routes> + <Route path="/feed/:feedId" element={<FeedItems />} /> + <Route path="/" element={<p>Select a feed to view items.</p>} /> + </Routes> </main> </div> </div> @@ -295,7 +307,7 @@ export default App; <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedItems.css.html b/frontend/coverage/src/components/FeedItems.css.html new file mode 100644 index 0000000..3951ead --- /dev/null +++ b/frontend/coverage/src/components/FeedItems.css.html @@ -0,0 +1,262 @@ + +<!doctype html> +<html lang="en"> + +<head> + <title>Code coverage report for src/components/FeedItems.css</title> + <meta charset="utf-8" /> + <link rel="stylesheet" href="../../prettify.css" /> + <link rel="stylesheet" href="../../base.css" /> + <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <style type='text/css'> + .coverage-summary .sorter { + background-image: url(../../sort-arrow-sprite.png); + } + </style> +</head> + +<body> +<div class='wrapper'> + <div class='pad1'> + <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItems.css</h1> + <div class='clearfix'> + + <div class='fl pad1y space-right2'> + <span class="strong">0% </span> + <span class="quiet">Statements</span> + <span class='fraction'>0/0</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">0% </span> + <span class="quiet">Branches</span> + <span class='fraction'>0/0</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">0% </span> + <span class="quiet">Functions</span> + <span class='fraction'>0/0</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">0% </span> + <span class="quiet">Lines</span> + <span class='fraction'>0/0</span> + </div> + + + </div> + <p class="quiet"> + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. + </p> + <template id="filterTemplate"> + <div class="quiet"> + Filter: + <input type="search" id="fileSearch"> + </div> + </template> + </div> + <div class='status-line low'></div> + <pre><table class="coverage"> +<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> +<a name='L2'></a><a href='#L2'>2</a> +<a name='L3'></a><a href='#L3'>3</a> +<a name='L4'></a><a href='#L4'>4</a> +<a name='L5'></a><a href='#L5'>5</a> +<a name='L6'></a><a href='#L6'>6</a> +<a name='L7'></a><a href='#L7'>7</a> +<a name='L8'></a><a href='#L8'>8</a> +<a name='L9'></a><a href='#L9'>9</a> +<a name='L10'></a><a href='#L10'>10</a> +<a name='L11'></a><a href='#L11'>11</a> +<a name='L12'></a><a href='#L12'>12</a> +<a name='L13'></a><a href='#L13'>13</a> +<a name='L14'></a><a href='#L14'>14</a> +<a name='L15'></a><a href='#L15'>15</a> +<a name='L16'></a><a href='#L16'>16</a> +<a name='L17'></a><a href='#L17'>17</a> +<a name='L18'></a><a href='#L18'>18</a> +<a name='L19'></a><a href='#L19'>19</a> +<a name='L20'></a><a href='#L20'>20</a> +<a name='L21'></a><a href='#L21'>21</a> +<a name='L22'></a><a href='#L22'>22</a> +<a name='L23'></a><a href='#L23'>23</a> +<a name='L24'></a><a href='#L24'>24</a> +<a name='L25'></a><a href='#L25'>25</a> +<a name='L26'></a><a href='#L26'>26</a> +<a name='L27'></a><a href='#L27'>27</a> +<a name='L28'></a><a href='#L28'>28</a> +<a name='L29'></a><a href='#L29'>29</a> +<a name='L30'></a><a href='#L30'>30</a> +<a name='L31'></a><a href='#L31'>31</a> +<a name='L32'></a><a href='#L32'>32</a> +<a name='L33'></a><a href='#L33'>33</a> +<a name='L34'></a><a href='#L34'>34</a> +<a name='L35'></a><a href='#L35'>35</a> +<a name='L36'></a><a href='#L36'>36</a> +<a name='L37'></a><a href='#L37'>37</a> +<a name='L38'></a><a href='#L38'>38</a> +<a name='L39'></a><a href='#L39'>39</a> +<a name='L40'></a><a href='#L40'>40</a> +<a name='L41'></a><a href='#L41'>41</a> +<a name='L42'></a><a href='#L42'>42</a> +<a name='L43'></a><a href='#L43'>43</a> +<a name='L44'></a><a href='#L44'>44</a> +<a name='L45'></a><a href='#L45'>45</a> +<a name='L46'></a><a href='#L46'>46</a> +<a name='L47'></a><a href='#L47'>47</a> +<a name='L48'></a><a href='#L48'>48</a> +<a name='L49'></a><a href='#L49'>49</a> +<a name='L50'></a><a href='#L50'>50</a> +<a name='L51'></a><a href='#L51'>51</a> +<a name='L52'></a><a href='#L52'>52</a> +<a name='L53'></a><a href='#L53'>53</a> +<a name='L54'></a><a href='#L54'>54</a> +<a name='L55'></a><a href='#L55'>55</a> +<a name='L56'></a><a href='#L56'>56</a> +<a name='L57'></a><a href='#L57'>57</a> +<a name='L58'></a><a href='#L58'>58</a> +<a name='L59'></a><a href='#L59'>59</a> +<a name='L60'></a><a href='#L60'>60</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">.feed-items { + padding: 1rem; +} + +.feed-items h2 { + margin-top: 0; + border-bottom: 2px solid #eee; + padding-bottom: 0.5rem; +} + +.item-list { + list-style: none; + padding: 0; +} + +.item { + border-bottom: 1px solid #f0f0f0; + padding: 1rem 0; +} + +.item.read .item-title { + color: #888; + font-weight: normal; +} + +.item.unread .item-title { + font-weight: bold; +} + +.item-title { + font-size: 1.2rem; + text-decoration: none; + color: #333; + display: block; + margin-bottom: 0.5rem; +} + +.item-title:hover { + text-decoration: underline; + color: #007bff; +} + +.item-meta { + font-size: 0.85rem; + color: #666; + margin-bottom: 0.5rem; +} + +.item-description { + color: #444; + line-height: 1.5; + font-size: 0.95rem; +} + +.item-description img { + max-width: 100%; + height: auto; + display: block; + margin: 1rem 0; +}</pre></td></tr></table></pre> + + <div class='push'></div><!-- for sticky footer --> + </div><!-- /wrapper --> + <div class='footer quiet pad2 space-top1 center small'> + Code coverage generated by + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> + at 2026-02-13T06:02:51.320Z + </div> + <script src="../../prettify.js"></script> + <script> + window.onload = function () { + prettyPrint(); + }; + </script> + <script src="../../sorter.js"></script> + <script src="../../block-navigation.js"></script> + </body> +</html> +
\ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html new file mode 100644 index 0000000..85c9705 --- /dev/null +++ b/frontend/coverage/src/components/FeedItems.tsx.html @@ -0,0 +1,283 @@ + +<!doctype html> +<html lang="en"> + +<head> + <title>Code coverage report for src/components/FeedItems.tsx</title> + <meta charset="utf-8" /> + <link rel="stylesheet" href="../../prettify.css" /> + <link rel="stylesheet" href="../../base.css" /> + <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <style type='text/css'> + .coverage-summary .sorter { + background-image: url(../../sort-arrow-sprite.png); + } + </style> +</head> + +<body> +<div class='wrapper'> + <div class='pad1'> + <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItems.tsx</h1> + <div class='clearfix'> + + <div class='fl pad1y space-right2'> + <span class="strong">86.36% </span> + <span class="quiet">Statements</span> + <span class='fraction'>19/22</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">61.11% </span> + <span class="quiet">Branches</span> + <span class='fraction'>11/18</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">83.33% </span> + <span class="quiet">Functions</span> + <span class='fraction'>5/6</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">85% </span> + <span class="quiet">Lines</span> + <span class='fraction'>17/20</span> + </div> + + + </div> + <p class="quiet"> + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. + </p> + <template id="filterTemplate"> + <div class="quiet"> + Filter: + <input type="search" id="fileSearch"> + </div> + </template> + </div> + <div class='status-line high'></div> + <pre><table class="coverage"> +<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> +<a name='L2'></a><a href='#L2'>2</a> +<a name='L3'></a><a href='#L3'>3</a> +<a name='L4'></a><a href='#L4'>4</a> +<a name='L5'></a><a href='#L5'>5</a> +<a name='L6'></a><a href='#L6'>6</a> +<a name='L7'></a><a href='#L7'>7</a> +<a name='L8'></a><a href='#L8'>8</a> +<a name='L9'></a><a href='#L9'>9</a> +<a name='L10'></a><a href='#L10'>10</a> +<a name='L11'></a><a href='#L11'>11</a> +<a name='L12'></a><a href='#L12'>12</a> +<a name='L13'></a><a href='#L13'>13</a> +<a name='L14'></a><a href='#L14'>14</a> +<a name='L15'></a><a href='#L15'>15</a> +<a name='L16'></a><a href='#L16'>16</a> +<a name='L17'></a><a href='#L17'>17</a> +<a name='L18'></a><a href='#L18'>18</a> +<a name='L19'></a><a href='#L19'>19</a> +<a name='L20'></a><a href='#L20'>20</a> +<a name='L21'></a><a href='#L21'>21</a> +<a name='L22'></a><a href='#L22'>22</a> +<a name='L23'></a><a href='#L23'>23</a> +<a name='L24'></a><a href='#L24'>24</a> +<a name='L25'></a><a href='#L25'>25</a> +<a name='L26'></a><a href='#L26'>26</a> +<a name='L27'></a><a href='#L27'>27</a> +<a name='L28'></a><a href='#L28'>28</a> +<a name='L29'></a><a href='#L29'>29</a> +<a name='L30'></a><a href='#L30'>30</a> +<a name='L31'></a><a href='#L31'>31</a> +<a name='L32'></a><a href='#L32'>32</a> +<a name='L33'></a><a href='#L33'>33</a> +<a name='L34'></a><a href='#L34'>34</a> +<a name='L35'></a><a href='#L35'>35</a> +<a name='L36'></a><a href='#L36'>36</a> +<a name='L37'></a><a href='#L37'>37</a> +<a name='L38'></a><a href='#L38'>38</a> +<a name='L39'></a><a href='#L39'>39</a> +<a name='L40'></a><a href='#L40'>40</a> +<a name='L41'></a><a href='#L41'>41</a> +<a name='L42'></a><a href='#L42'>42</a> +<a name='L43'></a><a href='#L43'>43</a> +<a name='L44'></a><a href='#L44'>44</a> +<a name='L45'></a><a href='#L45'>45</a> +<a name='L46'></a><a href='#L46'>46</a> +<a name='L47'></a><a href='#L47'>47</a> +<a name='L48'></a><a href='#L48'>48</a> +<a name='L49'></a><a href='#L49'>49</a> +<a name='L50'></a><a href='#L50'>50</a> +<a name='L51'></a><a href='#L51'>51</a> +<a name='L52'></a><a href='#L52'>52</a> +<a name='L53'></a><a href='#L53'>53</a> +<a name='L54'></a><a href='#L54'>54</a> +<a name='L55'></a><a href='#L55'>55</a> +<a name='L56'></a><a href='#L56'>56</a> +<a name='L57'></a><a href='#L57'>57</a> +<a name='L58'></a><a href='#L58'>58</a> +<a name='L59'></a><a href='#L59'>59</a> +<a name='L60'></a><a href='#L60'>60</a> +<a name='L61'></a><a href='#L61'>61</a> +<a name='L62'></a><a href='#L62'>62</a> +<a name='L63'></a><a href='#L63'>63</a> +<a name='L64'></a><a href='#L64'>64</a> +<a name='L65'></a><a href='#L65'>65</a> +<a name='L66'></a><a href='#L66'>66</a> +<a name='L67'></a><a href='#L67'>67</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">1x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import type { Item } from '../types'; +import './FeedItems.css'; + +export default function FeedItems() { + const { feedId } = useParams<{ feedId: string }>(); + const [items, setItems] = useState<Item[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + setLoading(true); + setError(''); + + const url = feedId + ? `/api/stream?feed_id=${feedId}` + : <span class="branch-1 cbranch-no" title="branch not covered" >'/api/stream'; // Default or "all" view? For now let's assume we need a feedId or handle "all" logic later</span> + + fetch(url) + .then((res) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) { +<span class="cstat-no" title="statement not covered" > throw new Error('Failed to fetch items');</span> + } + return res.json(); + }) + .then((data) => { + setItems(data); + setLoading(false); + }) + .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { +<span class="cstat-no" title="statement not covered" > setError(err.message);</span> +<span class="cstat-no" title="statement not covered" > setLoading(false);</span> + }); + }, [feedId]); + + if (loading) return <div className="feed-items-loading">Loading items...</div>; + <span class="missing-if-branch" title="if path not taken" >I</span>if (error) return <div className="feed-items-error">Error: {error}</div>; + + return ( + <div className="feed-items"> + <h2>Items</h2> + {/* TODO: Add Feed Title here if possible, maybe pass from location state or fetch feed details */} + {items.length === 0 ? ( +<span class="branch-0 cbranch-no" title="branch not covered" > <p>No items found.</p></span> + ) : ( + <ul className="item-list"> + {items.map((item) => ( + <li key={item._id} className={`item ${item.read ? 'read' : 'unread'}`}> + <a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title"> + {item.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}</span> + </a> + <div className="item-meta"> + <span className="item-date">{new Date(item.publish_date).toLocaleDateString()}</span> + {item.feed_title && <span class="branch-1 cbranch-no" title="branch not covered" ><span className="item-feed"> - {item.feed_title}</span>}</span> + </div> + {item.description && ( +<span class="branch-1 cbranch-no" title="branch not covered" > <div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} /></span> + )} + </li> + ))} + </ul> + )} + </div> + ); +} + </pre></td></tr></table></pre> + + <div class='push'></div><!-- for sticky footer --> + </div><!-- /wrapper --> + <div class='footer quiet pad2 space-top1 center small'> + Code coverage generated by + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> + at 2026-02-13T06:02:51.320Z + </div> + <script src="../../prettify.js"></script> + <script> + window.onload = function () { + prettyPrint(); + }; + </script> + <script src="../../sorter.js"></script> + <script src="../../block-navigation.js"></script> + </body> +</html> +
\ No newline at end of file diff --git a/frontend/coverage/src/components/FeedList.css.html b/frontend/coverage/src/components/FeedList.css.html index cd14b1b..f86a2d4 100644 --- a/frontend/coverage/src/components/FeedList.css.html +++ b/frontend/coverage/src/components/FeedList.css.html @@ -211,7 +211,7 @@ <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedList.tsx.html b/frontend/coverage/src/components/FeedList.tsx.html index d7344a2..e220912 100644 --- a/frontend/coverage/src/components/FeedList.tsx.html +++ b/frontend/coverage/src/components/FeedList.tsx.html @@ -113,16 +113,18 @@ <a name='L48'></a><a href='#L48'>48</a> <a name='L49'></a><a href='#L49'>49</a> <a name='L50'></a><a href='#L50'>50</a> -<a name='L51'></a><a href='#L51'>51</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<a name='L51'></a><a href='#L51'>51</a> +<a name='L52'></a><a href='#L52'>52</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> -<span class="cline-any cline-yes">9x</span> -<span class="cline-any cline-yes">9x</span> -<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-neutral"> </span> -<span class="cline-any cline-yes">9x</span> +<span class="cline-any cline-yes">8x</span> +<span class="cline-any cline-yes">8x</span> +<span class="cline-any cline-yes">8x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">5x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">3x</span> @@ -140,11 +142,11 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> -<span class="cline-any cline-yes">9x</span> -<span class="cline-any cline-yes">4x</span> -<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">3x</span> <span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">2x</span> +<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -164,6 +166,7 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import type { Feed } from '../types'; import './FeedList.css'; @@ -202,9 +205,9 @@ export default function FeedList() { <ul className="feed-list-items"> {feeds.map((feed) => ( <li key={feed._id} className="feed-item"> - <a href={feed.web_url} target="_blank" rel="noopener noreferrer" className="feed-title"> + <Link to={`/feed/${feed._id}`} className="feed-title"> {feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >feed.url}</span> - </a> + </Link> {feed.category && <span className="feed-category">{feed.category}</span>} </li> ))} @@ -220,7 +223,7 @@ export default function FeedList() { <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/Login.css.html b/frontend/coverage/src/components/Login.css.html index 2fb506b..5ef3e6c 100644 --- a/frontend/coverage/src/components/Login.css.html +++ b/frontend/coverage/src/components/Login.css.html @@ -259,7 +259,7 @@ button[type="submit"]:hover { <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/Login.tsx.html b/frontend/coverage/src/components/Login.tsx.html index 9d1052f..e58efff 100644 --- a/frontend/coverage/src/components/Login.tsx.html +++ b/frontend/coverage/src/components/Login.tsx.html @@ -232,7 +232,7 @@ export default function Login() { <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/index.html b/frontend/coverage/src/components/index.html index d9947b6..94dbb66 100644 --- a/frontend/coverage/src/components/index.html +++ b/frontend/coverage/src/components/index.html @@ -23,30 +23,30 @@ <div class='clearfix'> <div class='fl pad1y space-right2'> - <span class="strong">97.14% </span> + <span class="strong">92.98% </span> <span class="quiet">Statements</span> - <span class='fraction'>34/35</span> + <span class='fraction'>53/57</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">83.33% </span> + <span class="strong">72.22% </span> <span class="quiet">Branches</span> - <span class='fraction'>15/18</span> + <span class='fraction'>26/36</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">100% </span> + <span class="strong">93.33% </span> <span class="quiet">Functions</span> - <span class='fraction'>9/9</span> + <span class='fraction'>14/15</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">96.96% </span> + <span class="strong">92.45% </span> <span class="quiet">Lines</span> - <span class='fraction'>32/33</span> + <span class='fraction'>49/53</span> </div> @@ -79,6 +79,36 @@ </tr> </thead> <tbody><tr> + <td class="file empty" data-value="FeedItems.css"><a href="FeedItems.css.html">FeedItems.css</a></td> + <td data-value="0" class="pic empty"> + <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div> + </td> + <td data-value="0" class="pct empty">0%</td> + <td data-value="0" class="abs empty">0/0</td> + <td data-value="0" class="pct empty">0%</td> + <td data-value="0" class="abs empty">0/0</td> + <td data-value="0" class="pct empty">0%</td> + <td data-value="0" class="abs empty">0/0</td> + <td data-value="0" class="pct empty">0%</td> + <td data-value="0" class="abs empty">0/0</td> + </tr> + +<tr> + <td class="file high" data-value="FeedItems.tsx"><a href="FeedItems.tsx.html">FeedItems.tsx</a></td> + <td data-value="86.36" class="pic high"> + <div class="chart"><div class="cover-fill" style="width: 86%"></div><div class="cover-empty" style="width: 14%"></div></div> + </td> + <td data-value="86.36" class="pct high">86.36%</td> + <td data-value="22" class="abs high">19/22</td> + <td data-value="61.11" class="pct medium">61.11%</td> + <td data-value="18" class="abs medium">11/18</td> + <td data-value="83.33" class="pct high">83.33%</td> + <td data-value="6" class="abs high">5/6</td> + <td data-value="85" class="pct high">85%</td> + <td data-value="20" class="abs high">17/20</td> + </tr> + +<tr> <td class="file empty" data-value="FeedList.css"><a href="FeedList.css.html">FeedList.css</a></td> <td data-value="0" class="pic empty"> <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div> @@ -146,7 +176,7 @@ <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/index.html b/frontend/coverage/src/index.html index 864b5d1..01547dd 100644 --- a/frontend/coverage/src/index.html +++ b/frontend/coverage/src/index.html @@ -116,7 +116,7 @@ <div class='footer quiet pad2 space-top1 center small'> Code coverage generated by <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T05:59:33.745Z + at 2026-02-13T06:02:51.320Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7d26025..bc4e097 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ function RequireAuth({ children }: { children: React.ReactElement }) { } import FeedList from './components/FeedList'; +import FeedItems from './components/FeedItems'; function Dashboard() { return ( @@ -47,7 +48,10 @@ function Dashboard() { <FeedList /> </aside> <main className="dashboard-main"> - <p>Select a feed to view items.</p> + <Routes> + <Route path="/feed/:feedId" element={<FeedItems />} /> + <Route path="/" element={<p>Select a feed to view items.</p>} /> + </Routes> </main> </div> </div> diff --git a/frontend/src/components/FeedItems.css b/frontend/src/components/FeedItems.css new file mode 100644 index 0000000..a057a40 --- /dev/null +++ b/frontend/src/components/FeedItems.css @@ -0,0 +1,60 @@ +.feed-items { + padding: 1rem; +} + +.feed-items h2 { + margin-top: 0; + border-bottom: 2px solid #eee; + padding-bottom: 0.5rem; +} + +.item-list { + list-style: none; + padding: 0; +} + +.item { + border-bottom: 1px solid #f0f0f0; + padding: 1rem 0; +} + +.item.read .item-title { + color: #888; + font-weight: normal; +} + +.item.unread .item-title { + font-weight: bold; +} + +.item-title { + font-size: 1.2rem; + text-decoration: none; + color: #333; + display: block; + margin-bottom: 0.5rem; +} + +.item-title:hover { + text-decoration: underline; + color: #007bff; +} + +.item-meta { + font-size: 0.85rem; + color: #666; + margin-bottom: 0.5rem; +} + +.item-description { + color: #444; + line-height: 1.5; + font-size: 0.95rem; +} + +.item-description img { + max-width: 100%; + height: auto; + display: block; + margin: 1rem 0; +}
\ No newline at end of file diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx new file mode 100644 index 0000000..9c271c4 --- /dev/null +++ b/frontend/src/components/FeedItems.test.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import FeedItems from './FeedItems'; + +describe('FeedItems Component', () => { + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + }); + + it('renders loading state', () => { + (global.fetch as any).mockImplementation(() => new Promise(() => { })); + render( + <MemoryRouter initialEntries={['/feed/1']}> + <Routes> + <Route path="/feed/:feedId" element={<FeedItems />} /> + </Routes> + </MemoryRouter> + ); + expect(screen.getByText(/loading items/i)).toBeInTheDocument(); + }); + + it('renders items for a feed', async () => { + const mockItems = [ + { _id: 101, title: 'Item One', url: 'http://example.com/1', publish_date: '2023-01-01', read: false }, + { _id: 102, title: 'Item Two', url: 'http://example.com/2', publish_date: '2023-01-02', read: true }, + ]; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockItems, + }); + + render( + <MemoryRouter initialEntries={['/feed/1']}> + <Routes> + <Route path="/feed/:feedId" element={<FeedItems />} /> + </Routes> + </MemoryRouter> + ); + + await waitFor(() => { + expect(screen.getByText('Item One')).toBeInTheDocument(); + expect(screen.getByText('Item Two')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledWith('/api/stream?feed_id=1'); + }); +}); diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx new file mode 100644 index 0000000..048bed7 --- /dev/null +++ b/frontend/src/components/FeedItems.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import type { Item } from '../types'; +import './FeedItems.css'; + +export default function FeedItems() { + const { feedId } = useParams<{ feedId: string }>(); + const [items, setItems] = useState<Item[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + setLoading(true); + setError(''); + + const url = feedId + ? `/api/stream?feed_id=${feedId}` + : '/api/stream'; // Default or "all" view? For now let's assume we need a feedId or handle "all" logic later + + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error('Failed to fetch items'); + } + return res.json(); + }) + .then((data) => { + setItems(data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }, [feedId]); + + if (loading) return <div className="feed-items-loading">Loading items...</div>; + if (error) return <div className="feed-items-error">Error: {error}</div>; + + return ( + <div className="feed-items"> + <h2>Items</h2> + {/* TODO: Add Feed Title here if possible, maybe pass from location state or fetch feed details */} + {items.length === 0 ? ( + <p>No items found.</p> + ) : ( + <ul className="item-list"> + {items.map((item) => ( + <li key={item._id} className={`item ${item.read ? 'read' : 'unread'}`}> + <a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title"> + {item.title || '(No Title)'} + </a> + <div className="item-meta"> + <span className="item-date">{new Date(item.publish_date).toLocaleDateString()}</span> + {item.feed_title && <span className="item-feed"> - {item.feed_title}</span>} + </div> + {item.description && ( + <div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} /> + )} + </li> + ))} + </ul> + )} + </div> + ); +} diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index 578e3c2..92ff345 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -4,6 +4,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import FeedList from './FeedList'; +import { BrowserRouter } from 'react-router-dom'; + describe('FeedList Component', () => { beforeEach(() => { vi.resetAllMocks(); @@ -12,7 +14,11 @@ describe('FeedList Component', () => { it('renders loading state initially', () => { (global.fetch as any).mockImplementation(() => new Promise(() => { })); - render(<FeedList />); + render( + <BrowserRouter> + <FeedList /> + </BrowserRouter> + ); expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); }); @@ -27,7 +33,11 @@ describe('FeedList Component', () => { json: async () => mockFeeds, }); - render(<FeedList />); + render( + <BrowserRouter> + <FeedList /> + </BrowserRouter> + ); await waitFor(() => { expect(screen.getByText('Feed One')).toBeInTheDocument(); @@ -39,7 +49,11 @@ describe('FeedList Component', () => { it('handles fetch error', async () => { (global.fetch as any).mockRejectedValueOnce(new Error('API Error')); - render(<FeedList />); + render( + <BrowserRouter> + <FeedList /> + </BrowserRouter> + ); await waitFor(() => { expect(screen.getByText(/error: api error/i)).toBeInTheDocument(); @@ -52,7 +66,11 @@ describe('FeedList Component', () => { json: async () => [], }); - render(<FeedList />); + render( + <BrowserRouter> + <FeedList /> + </BrowserRouter> + ); await waitFor(() => { expect(screen.getByText(/no feeds found/i)).toBeInTheDocument(); diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index fb7c1de..f913293 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import type { Feed } from '../types'; import './FeedList.css'; @@ -37,9 +38,9 @@ export default function FeedList() { <ul className="feed-list-items"> {feeds.map((feed) => ( <li key={feed._id} className="feed-item"> - <a href={feed.web_url} target="_blank" rel="noopener noreferrer" className="feed-title"> + <Link to={`/feed/${feed._id}`} className="feed-title"> {feed.title || feed.url} - </a> + </Link> {feed.category && <span className="feed-category">{feed.category}</span>} </li> ))} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 905b1dc..872e608 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -5,3 +5,17 @@ export interface Feed { title: string; category: string; } + +export interface Item { + _id: number; + feed_id: number; + title: string; + url: string; + description: string; + publish_date: string; + read: boolean; + starred: boolean; + full_content?: string; + header_image?: string; + feed_title?: string; +} |
