diff options
22 files changed, 1290 insertions, 84 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index 312d198..ef27c08 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -1,5 +1,5 @@ {"id":"NK-0nf7hu","title":"Implement Frontend Logout","description":"Add logout button/link in dashboard and call /api/logout.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:50:46.760744241Z","updated":"2026-02-13T05:50:46.760744241Z"} -{"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-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-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"} @@ -11,19 +11,23 @@ {"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":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:55:14.825454967Z","updated":"2026-02-13T14:58:18.307521003Z"} {"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-mwf9q2","title":"Implement Tag View","description":"Create frontend view for browsing items by tag/category. Use /tag/:id endpoint.","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:04:12.441165286Z","updated":"2026-02-13T15:04:12.441165286Z"} {"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"} +{"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-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":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:01:33.817314728Z","updated":"2026-02-13T15:01:33.817314728Z"} {"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-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"} +{"id":"NK-d0lgaab","from_ticket_id":"NK-sne5ox","to_ticket_id":"NK-0ppv3f","type":"created_from","created":"2026-02-13T15:05:23.289745853Z"} {"id":"NK-d1uyy71","from_ticket_id":"NK-27or4b","to_ticket_id":"NK-bsdwqz","type":"created_from","created":"2026-02-13T05:03:09.689282214Z"} {"id":"NK-d50pbhs","from_ticket_id":"NK-zt4e32","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.598803513Z"} {"id":"NK-d86tgcs","from_ticket_id":"NK-ed1iah","to_ticket_id":"NK-1phdpf","type":"created_from","created":"2026-02-13T04:26:55.917754798Z"} {"id":"NK-dew7hvb","from_ticket_id":"NK-tw0nga","to_ticket_id":"NK-59kbij","type":"created_from","created":"2026-02-13T15:01:33.825547908Z"} {"id":"NK-dgbrb79","from_ticket_id":"NK-9hx0y7","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.556027956Z"} {"id":"NK-dgfppki","from_ticket_id":"NK-gqkh96","to_ticket_id":"NK-x924bu","type":"created_from","created":"2026-02-13T03:54:30.303602703Z"} +{"id":"NK-dkhu8ov","from_ticket_id":"NK-mwf9q2","to_ticket_id":"NK-0ppv3f","type":"created_from","created":"2026-02-13T15:04:12.449401674Z"} {"id":"NK-dl8clj9","from_ticket_id":"NK-0nf7hu","to_ticket_id":"NK-9hx0y7","type":"created_from","created":"2026-02-13T05:50:46.769436228Z"} {"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"} diff --git a/frontend/coverage/clover.xml b/frontend/coverage/clover.xml index a6ecc62..26fec33 100644 --- a/frontend/coverage/clover.xml +++ b/frontend/coverage/clover.xml @@ -1,14 +1,14 @@ <?xml version="1.0" encoding="UTF-8"?> -<coverage generated="1770994870810" clover="3.2.0"> - <project timestamp="1770994870810" name="All files"> - <metrics statements="89" coveredstatements="81" conditionals="58" coveredconditionals="47" methods="30" coveredmethods="28" elements="177" coveredelements="156" complexity="0" loc="89" ncloc="89" packages="2" files="10" classes="10"/> +<coverage generated="1770995023032" clover="3.2.0"> + <project timestamp="1770995023032" name="All files"> + <metrics statements="130" coveredstatements="114" conditionals="74" coveredconditionals="56" methods="48" coveredmethods="42" elements="252" coveredelements="212" complexity="0" loc="130" ncloc="130" packages="2" files="12" classes="12"/> <package name="src"> - <metrics statements="17" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="8" coveredmethods="7"/> + <metrics statements="19" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="9" coveredmethods="7"/> <file name="App.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.css"> <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> </file> <file name="App.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx"> - <metrics statements="17" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="8" coveredmethods="7"/> + <metrics statements="19" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="9" coveredmethods="7"/> <line num="8" count="2" type="stmt"/> <line num="9" count="2" type="stmt"/> <line num="11" count="2" type="stmt"/> @@ -22,14 +22,16 @@ <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="38" count="1" type="stmt"/> - <line num="44" count="1" type="stmt"/> - <line num="45" count="1" type="stmt"/> - <line num="67" count="2" type="stmt"/> + <line num="39" count="1" type="stmt"/> + <line num="45" count="0" type="stmt"/> + <line num="46" count="0" type="stmt"/> + <line num="55" count="1" type="stmt"/> + <line num="56" count="1" type="stmt"/> + <line num="79" count="2" type="stmt"/> </file> </package> <package name="src.components"> - <metrics statements="72" coveredstatements="67" conditionals="52" coveredconditionals="43" methods="22" coveredmethods="21"/> + <metrics statements="111" coveredstatements="100" conditionals="68" coveredconditionals="52" methods="39" coveredmethods="35"/> <file name="FeedItem.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css"> <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> </file> @@ -126,6 +128,51 @@ <line num="35" count="14" type="stmt"/> <line num="45" count="3" type="stmt"/> </file> + <file name="Settings.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css"> + <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/> + </file> + <file name="Settings.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx"> + <metrics statements="39" coveredstatements="33" conditionals="16" coveredconditionals="9" methods="17" coveredmethods="14"/> + <line num="6" count="14" type="stmt"/> + <line num="7" count="14" type="stmt"/> + <line num="8" count="14" type="stmt"/> + <line num="9" count="14" type="stmt"/> + <line num="11" count="14" type="stmt"/> + <line num="12" count="3" type="stmt"/> + <line num="15" count="14" type="stmt"/> + <line num="16" count="4" type="stmt"/> + <line num="17" count="4" type="stmt"/> + <line num="19" count="4" type="cond" truecount="1" falsecount="1"/> + <line num="20" count="4" type="stmt"/> + <line num="23" count="4" type="stmt"/> + <line num="24" count="4" type="stmt"/> + <line num="27" count="0" type="stmt"/> + <line num="28" count="0" type="stmt"/> + <line num="32" count="14" type="stmt"/> + <line num="33" count="1" type="stmt"/> + <line num="34" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="36" count="1" type="stmt"/> + <line num="37" count="1" type="stmt"/> + <line num="43" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="44" count="1" type="stmt"/> + <line num="47" count="1" type="stmt"/> + <line num="48" count="1" type="stmt"/> + <line num="51" count="0" type="stmt"/> + <line num="52" count="0" type="stmt"/> + <line num="56" count="14" type="stmt"/> + <line num="57" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="59" count="1" type="stmt"/> + <line num="60" count="1" type="stmt"/> + <line num="64" count="1" type="cond" truecount="1" falsecount="1"/> + <line num="65" count="1" type="stmt"/> + <line num="66" count="1" type="stmt"/> + <line num="69" count="0" type="stmt"/> + <line num="70" count="0" type="stmt"/> + <line num="74" count="14" type="stmt"/> + <line num="84" count="1" type="stmt"/> + <line num="102" count="5" type="stmt"/> + <line num="108" count="1" type="stmt"/> + </file> </package> </project> </coverage> diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json index 2a5fa12..14d4351 100644 --- a/frontend/coverage/coverage-final.json +++ b/frontend/coverage/coverage-final.json @@ -1,5 +1,5 @@ {"/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":38,"column":2},"end":{"line":62,"column":null}},"14":{"start":{"line":44,"column":12},"end":{"line":45,"column":null}},"15":{"start":{"line":45,"column":26},"end":{"line":45,"column":58}},"16":{"start":{"line":67,"column":2},"end":{"line":80,"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":64,"column":null}},"line":37},"5":{"name":"(anonymous_5)","decl":{"start":{"line":43,"column":27},"end":{"line":43,"column":33}},"loc":{"start":{"line":43,"column":33},"end":{"line":46,"column":13}},"line":43},"6":{"name":"(anonymous_6)","decl":{"start":{"line":45,"column":20},"end":{"line":45,"column":26}},"loc":{"start":{"line":45,"column":26},"end":{"line":45,"column":58}},"line":45},"7":{"name":"App","decl":{"start":{"line":66,"column":9},"end":{"line":66,"column":15}},"loc":{"start":{"line":66,"column":15},"end":{"line":82,"column":null}},"line":66}},"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":1,"15":1,"16":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":1,"6":1,"7":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":8,"lastStatement":17,"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:62:Infinity":13,"f:43:27:43:33":5,"s:44:12:45:Infinity":14,"f:45:20:45:26":6,"s:45:26:45:58":15,"f:66:9:66:15":7,"s:67:2:80:Infinity":16}}} +,"/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":39,"column":2},"end":{"line":74,"column":null}},"14":{"start":{"line":45,"column":12},"end":{"line":45,"column":null}},"15":{"start":{"line":46,"column":12},"end":{"line":46,"column":null}},"16":{"start":{"line":55,"column":12},"end":{"line":56,"column":null}},"17":{"start":{"line":56,"column":26},"end":{"line":56,"column":58}},"18":{"start":{"line":79,"column":2},"end":{"line":92,"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":38,"column":9},"end":{"line":38,"column":21}},"loc":{"start":{"line":38,"column":21},"end":{"line":76,"column":null}},"line":38},"5":{"name":"(anonymous_5)","decl":{"start":{"line":44,"column":39},"end":{"line":44,"column":40}},"loc":{"start":{"line":44,"column":46},"end":{"line":52,"column":13}},"line":44},"6":{"name":"(anonymous_6)","decl":{"start":{"line":54,"column":27},"end":{"line":54,"column":33}},"loc":{"start":{"line":54,"column":33},"end":{"line":57,"column":13}},"line":54},"7":{"name":"(anonymous_7)","decl":{"start":{"line":56,"column":20},"end":{"line":56,"column":26}},"loc":{"start":{"line":56,"column":26},"end":{"line":56,"column":58}},"line":56},"8":{"name":"App","decl":{"start":{"line":78,"column":9},"end":{"line":78,"column":15}},"loc":{"start":{"line":78,"column":15},"end":{"line":94,"column":null}},"line":78}},"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":0,"15":0,"16":1,"17":1,"18":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":0,"6":1,"7":1,"8":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":9,"lastStatement":19,"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:38:9:38:21":4,"s:39:2:74:Infinity":13,"f:44:39:44:40":5,"s:45:12:45:Infinity":14,"s:46:12:46:Infinity":15,"f:54:27:54:33":6,"s:55:12:56:Infinity":16,"f:56:20:56:26":7,"s:56:26:56:58":17,"f:78:9:78:15":8,"s:79:2:92:Infinity":18}}} ,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} ,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx","statementMap":{"0":{"start":{"line":10,"column":24},"end":{"line":10,"column":null}},"1":{"start":{"line":11,"column":30},"end":{"line":11,"column":null}},"2":{"start":{"line":13,"column":23},"end":{"line":15,"column":null}},"3":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"4":{"start":{"line":17,"column":23},"end":{"line":19,"column":null}},"5":{"start":{"line":18,"column":8},"end":{"line":18,"column":null}},"6":{"start":{"line":21,"column":23},"end":{"line":55,"column":null}},"7":{"start":{"line":22,"column":8},"end":{"line":22,"column":null}},"8":{"start":{"line":24,"column":29},"end":{"line":24,"column":null}},"9":{"start":{"line":25,"column":8},"end":{"line":25,"column":null}},"10":{"start":{"line":27,"column":8},"end":{"line":54,"column":null}},"11":{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},"12":{"start":{"line":40,"column":20},"end":{"line":40,"column":null}},"13":{"start":{"line":42,"column":16},"end":{"line":42,"column":null}},"14":{"start":{"line":47,"column":16},"end":{"line":47,"column":null}},"15":{"start":{"line":50,"column":16},"end":{"line":50,"column":null}},"16":{"start":{"line":52,"column":16},"end":{"line":52,"column":null}},"17":{"start":{"line":53,"column":16},"end":{"line":53,"column":null}},"18":{"start":{"line":57,"column":4},"end":{"line":87,"column":null}}},"fnMap":{"0":{"name":"FeedItem","decl":{"start":{"line":9,"column":24},"end":{"line":9,"column":33}},"loc":{"start":{"line":9,"column":71},"end":{"line":89,"column":null}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":13,"column":23},"end":{"line":13,"column":29}},"loc":{"start":{"line":13,"column":29},"end":{"line":15,"column":null}},"line":13},"2":{"name":"(anonymous_2)","decl":{"start":{"line":17,"column":23},"end":{"line":17,"column":29}},"loc":{"start":{"line":17,"column":29},"end":{"line":19,"column":null}},"line":17},"3":{"name":"(anonymous_3)","decl":{"start":{"line":21,"column":23},"end":{"line":21,"column":24}},"loc":{"start":{"line":21,"column":42},"end":{"line":55,"column":null}},"line":21},"4":{"name":"(anonymous_4)","decl":{"start":{"line":38,"column":18},"end":{"line":38,"column":19}},"loc":{"start":{"line":38,"column":27},"end":{"line":43,"column":13}},"line":38},"5":{"name":"(anonymous_5)","decl":{"start":{"line":44,"column":18},"end":{"line":44,"column":24}},"loc":{"start":{"line":44,"column":24},"end":{"line":48,"column":13}},"line":44},"6":{"name":"(anonymous_6)","decl":{"start":{"line":49,"column":19},"end":{"line":49,"column":20}},"loc":{"start":{"line":49,"column":28},"end":{"line":54,"column":13}},"line":49}},"branchMap":{"0":{"loc":{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},"type":"if","locations":[{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},{"start":{},"end":{}}],"line":39},"1":{"loc":{"start":{"line":58,"column":36},"end":{"line":58,"column":65}},"type":"cond-expr","locations":[{"start":{"line":58,"column":48},"end":{"line":58,"column":57}},{"start":{"line":58,"column":57},"end":{"line":58,"column":65}}],"line":58},"2":{"loc":{"start":{"line":58,"column":69},"end":{"line":58,"column":93}},"type":"cond-expr","locations":[{"start":{"line":58,"column":79},"end":{"line":58,"column":91}},{"start":{"line":58,"column":91},"end":{"line":58,"column":93}}],"line":58},"3":{"loc":{"start":{"line":61,"column":21},"end":{"line":61,"column":null}},"type":"binary-expr","locations":[{"start":{"line":61,"column":21},"end":{"line":61,"column":35}},{"start":{"line":61,"column":35},"end":{"line":61,"column":null}}],"line":61},"4":{"loc":{"start":{"line":66,"column":49},"end":{"line":66,"column":84}},"type":"cond-expr","locations":[{"start":{"line":66,"column":61},"end":{"line":66,"column":73}},{"start":{"line":66,"column":73},"end":{"line":66,"column":84}}],"line":66},"5":{"loc":{"start":{"line":67,"column":31},"end":{"line":67,"column":null}},"type":"cond-expr","locations":[{"start":{"line":67,"column":43},"end":{"line":67,"column":62}},{"start":{"line":67,"column":62},"end":{"line":67,"column":null}}],"line":67},"6":{"loc":{"start":{"line":69,"column":25},"end":{"line":69,"column":null}},"type":"cond-expr","locations":[{"start":{"line":69,"column":37},"end":{"line":69,"column":44}},{"start":{"line":69,"column":44},"end":{"line":69,"column":null}}],"line":69},"7":{"loc":{"start":{"line":73,"column":49},"end":{"line":73,"column":93}},"type":"cond-expr","locations":[{"start":{"line":73,"column":64},"end":{"line":73,"column":79}},{"start":{"line":73,"column":79},"end":{"line":73,"column":93}}],"line":73},"8":{"loc":{"start":{"line":74,"column":31},"end":{"line":74,"column":null}},"type":"cond-expr","locations":[{"start":{"line":74,"column":46},"end":{"line":74,"column":57}},{"start":{"line":74,"column":57},"end":{"line":74,"column":null}}],"line":74},"9":{"loc":{"start":{"line":76,"column":25},"end":{"line":76,"column":null}},"type":"cond-expr","locations":[{"start":{"line":76,"column":40},"end":{"line":76,"column":46}},{"start":{"line":76,"column":46},"end":{"line":76,"column":null}}],"line":76},"10":{"loc":{"start":{"line":82,"column":17},"end":{"line":82,"column":null}},"type":"binary-expr","locations":[{"start":{"line":82,"column":17},"end":{"line":82,"column":36}},{"start":{"line":82,"column":36},"end":{"line":82,"column":null}}],"line":82},"11":{"loc":{"start":{"line":84,"column":13},"end":{"line":85,"column":null}},"type":"binary-expr","locations":[{"start":{"line":84,"column":13},"end":{"line":84,"column":null}},{"start":{"line":85,"column":16},"end":{"line":85,"column":null}}],"line":84}},"s":{"0":12,"1":12,"2":12,"3":2,"4":12,"5":1,"6":12,"7":3,"8":3,"9":3,"10":3,"11":2,"12":0,"13":2,"14":2,"15":1,"16":1,"17":1,"18":12},"f":{"0":12,"1":2,"2":1,"3":3,"4":2,"5":2,"6":1},"b":{"0":[0,2],"1":[4,8],"2":[3,9],"3":[12,0],"4":[4,8],"5":[4,8],"6":[4,8],"7":[2,10],"8":[2,10],"9":[2,10],"10":[12,10],"11":[12,10]},"meta":{"lastBranch":12,"lastFunction":7,"lastStatement":19,"seen":{"f:9:24:9:33":0,"s:10:24:10:Infinity":0,"s:11:30:11:Infinity":1,"s:13:23:15:Infinity":2,"f:13:23:13:29":1,"s:14:8:14:Infinity":3,"s:17:23:19:Infinity":4,"f:17:23:17:29":2,"s:18:8:18:Infinity":5,"s:21:23:55:Infinity":6,"f:21:23:21:24":3,"s:22:8:22:Infinity":7,"s:24:29:24:Infinity":8,"s:25:8:25:Infinity":9,"s:27:8:54:Infinity":10,"f:38:18:38:19":4,"b:39:16:41:Infinity:undefined:undefined:undefined:undefined":0,"s:39:16:41:Infinity":11,"s:40:20:40:Infinity":12,"s:42:16:42:Infinity":13,"f:44:18:44:24":5,"s:47:16:47:Infinity":14,"f:49:19:49:20":6,"s:50:16:50:Infinity":15,"s:52:16:52:Infinity":16,"s:53:16:53:Infinity":17,"s:57:4:87:Infinity":18,"b:58:48:58:57:58:57:58:65":1,"b:58:79:58:91:58:91:58:93":2,"b:61:21:61:35:61:35:61:Infinity":3,"b:66:61:66:73:66:73:66:84":4,"b:67:43:67:62:67:62:67:Infinity":5,"b:69:37:69:44:69:44:69:Infinity":6,"b:73:64:73:79:73:79:73:93":7,"b:74:46:74:57:74:57:74:Infinity":8,"b:76:40:76:46:76:46:76:Infinity":9,"b:82:17:82:36:82:36:82:Infinity":10,"b:84:13:84:Infinity:85:16:85:Infinity":11}}} ,"/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":{}}} @@ -8,4 +8,6 @@ ,"/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":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: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}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} +,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx","statementMap":{"0":{"start":{"line":6,"column":26},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":36},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":30},"end":{"line":8,"column":null}},"3":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"4":{"start":{"line":11,"column":4},"end":{"line":13,"column":null}},"5":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"6":{"start":{"line":15,"column":23},"end":{"line":30,"column":null}},"7":{"start":{"line":16,"column":8},"end":{"line":16,"column":null}},"8":{"start":{"line":17,"column":8},"end":{"line":29,"column":null}},"9":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"10":{"start":{"line":19,"column":29},"end":{"line":19,"column":null}},"11":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"12":{"start":{"line":23,"column":16},"end":{"line":23,"column":null}},"13":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"14":{"start":{"line":27,"column":16},"end":{"line":27,"column":null}},"15":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"16":{"start":{"line":32,"column":26},"end":{"line":54,"column":null}},"17":{"start":{"line":33,"column":8},"end":{"line":33,"column":null}},"18":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"19":{"start":{"line":34,"column":25},"end":{"line":34,"column":null}},"20":{"start":{"line":36,"column":8},"end":{"line":36,"column":null}},"21":{"start":{"line":37,"column":8},"end":{"line":53,"column":null}},"22":{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},"23":{"start":{"line":43,"column":29},"end":{"line":43,"column":null}},"24":{"start":{"line":44,"column":16},"end":{"line":44,"column":null}},"25":{"start":{"line":47,"column":16},"end":{"line":47,"column":null}},"26":{"start":{"line":48,"column":16},"end":{"line":48,"column":null}},"27":{"start":{"line":51,"column":16},"end":{"line":51,"column":null}},"28":{"start":{"line":52,"column":16},"end":{"line":52,"column":null}},"29":{"start":{"line":56,"column":29},"end":{"line":72,"column":null}},"30":{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},"31":{"start":{"line":57,"column":79},"end":{"line":57,"column":null}},"32":{"start":{"line":59,"column":8},"end":{"line":59,"column":null}},"33":{"start":{"line":60,"column":8},"end":{"line":71,"column":null}},"34":{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},"35":{"start":{"line":64,"column":29},"end":{"line":64,"column":null}},"36":{"start":{"line":65,"column":16},"end":{"line":65,"column":null}},"37":{"start":{"line":65,"column":45},"end":{"line":65,"column":57}},"38":{"start":{"line":66,"column":16},"end":{"line":66,"column":null}},"39":{"start":{"line":69,"column":16},"end":{"line":69,"column":null}},"40":{"start":{"line":70,"column":16},"end":{"line":70,"column":null}},"41":{"start":{"line":74,"column":4},"end":{"line":119,"column":null}},"42":{"start":{"line":84,"column":41},"end":{"line":84,"column":null}},"43":{"start":{"line":102,"column":24},"end":{"line":115,"column":null}},"44":{"start":{"line":108,"column":47},"end":{"line":108,"column":null}}},"fnMap":{"0":{"name":"Settings","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":35}},"loc":{"start":{"line":5,"column":35},"end":{"line":121,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":20}},"loc":{"start":{"line":11,"column":20},"end":{"line":13,"column":7}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":23},"end":{"line":15,"column":29}},"loc":{"start":{"line":15,"column":29},"end":{"line":30,"column":null}},"line":15},"3":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":18},"end":{"line":18,"column":19}},"loc":{"start":{"line":18,"column":27},"end":{"line":21,"column":13}},"line":18},"4":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":18},"end":{"line":22,"column":19}},"loc":{"start":{"line":22,"column":28},"end":{"line":25,"column":13}},"line":22},"5":{"name":"(anonymous_5)","decl":{"start":{"line":26,"column":19},"end":{"line":26,"column":20}},"loc":{"start":{"line":26,"column":28},"end":{"line":29,"column":13}},"line":26},"6":{"name":"(anonymous_6)","decl":{"start":{"line":32,"column":26},"end":{"line":32,"column":27}},"loc":{"start":{"line":32,"column":50},"end":{"line":54,"column":null}},"line":32},"7":{"name":"(anonymous_7)","decl":{"start":{"line":42,"column":18},"end":{"line":42,"column":19}},"loc":{"start":{"line":42,"column":27},"end":{"line":45,"column":13}},"line":42},"8":{"name":"(anonymous_8)","decl":{"start":{"line":46,"column":18},"end":{"line":46,"column":24}},"loc":{"start":{"line":46,"column":24},"end":{"line":49,"column":13}},"line":46},"9":{"name":"(anonymous_9)","decl":{"start":{"line":50,"column":19},"end":{"line":50,"column":20}},"loc":{"start":{"line":50,"column":28},"end":{"line":53,"column":13}},"line":50},"10":{"name":"(anonymous_10)","decl":{"start":{"line":56,"column":29},"end":{"line":56,"column":30}},"loc":{"start":{"line":56,"column":45},"end":{"line":72,"column":null}},"line":56},"11":{"name":"(anonymous_11)","decl":{"start":{"line":63,"column":18},"end":{"line":63,"column":19}},"loc":{"start":{"line":63,"column":27},"end":{"line":67,"column":13}},"line":63},"12":{"name":"(anonymous_12)","decl":{"start":{"line":65,"column":38},"end":{"line":65,"column":39}},"loc":{"start":{"line":65,"column":45},"end":{"line":65,"column":57}},"line":65},"13":{"name":"(anonymous_13)","decl":{"start":{"line":68,"column":19},"end":{"line":68,"column":20}},"loc":{"start":{"line":68,"column":28},"end":{"line":71,"column":13}},"line":68},"14":{"name":"(anonymous_14)","decl":{"start":{"line":84,"column":34},"end":{"line":84,"column":35}},"loc":{"start":{"line":84,"column":41},"end":{"line":84,"column":null}},"line":84},"15":{"name":"(anonymous_15)","decl":{"start":{"line":101,"column":31},"end":{"line":101,"column":32}},"loc":{"start":{"line":102,"column":24},"end":{"line":115,"column":null}},"line":102},"16":{"name":"(anonymous_16)","decl":{"start":{"line":108,"column":41},"end":{"line":108,"column":47}},"loc":{"start":{"line":108,"column":47},"end":{"line":108,"column":null}},"line":108}},"branchMap":{"0":{"loc":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"type":"if","locations":[{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},{"start":{},"end":{}}],"line":19},"1":{"loc":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"type":"if","locations":[{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},{"start":{},"end":{}}],"line":34},"2":{"loc":{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},"type":"if","locations":[{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},{"start":{},"end":{}}],"line":43},"3":{"loc":{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},{"start":{},"end":{}}],"line":57},"4":{"loc":{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},"type":"if","locations":[{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},{"start":{},"end":{}}],"line":64},"5":{"loc":{"start":{"line":94,"column":17},"end":{"line":94,"column":null}},"type":"binary-expr","locations":[{"start":{"line":94,"column":17},"end":{"line":94,"column":26}},{"start":{"line":94,"column":26},"end":{"line":94,"column":null}}],"line":94},"6":{"loc":{"start":{"line":99,"column":17},"end":{"line":99,"column":null}},"type":"binary-expr","locations":[{"start":{"line":99,"column":17},"end":{"line":99,"column":28}},{"start":{"line":99,"column":28},"end":{"line":99,"column":null}}],"line":99},"7":{"loc":{"start":{"line":104,"column":62},"end":{"line":104,"column":89}},"type":"binary-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":104,"column":76}},{"start":{"line":104,"column":76},"end":{"line":104,"column":89}}],"line":104}},"s":{"0":14,"1":14,"2":14,"3":14,"4":14,"5":3,"6":14,"7":4,"8":4,"9":4,"10":0,"11":4,"12":4,"13":4,"14":0,"15":0,"16":14,"17":1,"18":1,"19":0,"20":1,"21":1,"22":1,"23":0,"24":1,"25":1,"26":1,"27":0,"28":0,"29":14,"30":1,"31":0,"32":1,"33":1,"34":1,"35":0,"36":1,"37":1,"38":1,"39":0,"40":0,"41":14,"42":1,"43":5,"44":1},"f":{"0":14,"1":3,"2":4,"3":4,"4":4,"5":0,"6":1,"7":1,"8":1,"9":0,"10":1,"11":1,"12":1,"13":0,"14":1,"15":5,"16":1},"b":{"0":[0,4],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[14,0],"6":[14,5],"7":[5,0]},"meta":{"lastBranch":8,"lastFunction":17,"lastStatement":45,"seen":{"f:5:24:5:35":0,"s:6:26:6:Infinity":0,"s:7:36:7:Infinity":1,"s:8:30:8:Infinity":2,"s:9:26:9:Infinity":3,"s:11:4:13:Infinity":4,"f:11:14:11:20":1,"s:12:8:12:Infinity":5,"s:15:23:30:Infinity":6,"f:15:23:15:29":2,"s:16:8:16:Infinity":7,"s:17:8:29:Infinity":8,"f:18:18:18:19":3,"b:19:16:19:Infinity:undefined:undefined:undefined:undefined":0,"s:19:16:19:Infinity":9,"s:19:29:19:Infinity":10,"s:20:16:20:Infinity":11,"f:22:18:22:19":4,"s:23:16:23:Infinity":12,"s:24:16:24:Infinity":13,"f:26:19:26:20":5,"s:27:16:27:Infinity":14,"s:28:16:28:Infinity":15,"s:32:26:54:Infinity":16,"f:32:26:32:27":6,"s:33:8:33:Infinity":17,"b:34:8:34:Infinity:undefined:undefined:undefined:undefined":1,"s:34:8:34:Infinity":18,"s:34:25:34:Infinity":19,"s:36:8:36:Infinity":20,"s:37:8:53:Infinity":21,"f:42:18:42:19":7,"b:43:16:43:Infinity:undefined:undefined:undefined:undefined":2,"s:43:16:43:Infinity":22,"s:43:29:43:Infinity":23,"s:44:16:44:Infinity":24,"f:46:18:46:24":8,"s:47:16:47:Infinity":25,"s:48:16:48:Infinity":26,"f:50:19:50:20":9,"s:51:16:51:Infinity":27,"s:52:16:52:Infinity":28,"s:56:29:72:Infinity":29,"f:56:29:56:30":10,"b:57:8:57:Infinity:undefined:undefined:undefined:undefined":3,"s:57:8:57:Infinity":30,"s:57:79:57:Infinity":31,"s:59:8:59:Infinity":32,"s:60:8:71:Infinity":33,"f:63:18:63:19":11,"b:64:16:64:Infinity:undefined:undefined:undefined:undefined":4,"s:64:16:64:Infinity":34,"s:64:29:64:Infinity":35,"s:65:16:65:Infinity":36,"f:65:38:65:39":12,"s:65:45:65:57":37,"s:66:16:66:Infinity":38,"f:68:19:68:20":13,"s:69:16:69:Infinity":39,"s:70:16:70:Infinity":40,"s:74:4:119:Infinity":41,"f:84:34:84:35":14,"s:84:41:84:Infinity":42,"b:94:17:94:26:94:26:94:Infinity":5,"b:99:17:99:28:99:28:99:Infinity":6,"f:101:31:101:32":15,"s:102:24:115:Infinity":43,"b:104:62:104:76:104:76:104:89":7,"f:108:41:108:47":16,"s:108:47:108:Infinity":44}}} } diff --git a/frontend/coverage/index.html b/frontend/coverage/index.html index 51ed5a6..279afdf 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">91.39% </span> + <span class="strong">85% </span> <span class="quiet">Statements</span> - <span class='fraction'>85/93</span> + <span class='fraction'>119/140</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">81.03% </span> + <span class="strong">75.67% </span> <span class="quiet">Branches</span> - <span class='fraction'>47/58</span> + <span class='fraction'>56/74</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">93.33% </span> + <span class="strong">87.5% </span> <span class="quiet">Functions</span> - <span class='fraction'>28/30</span> + <span class='fraction'>42/48</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">91.01% </span> + <span class="strong">87.69% </span> <span class="quiet">Lines</span> - <span class='fraction'>81/89</span> + <span class='fraction'>114/130</span> </div> @@ -79,33 +79,33 @@ </tr> </thead> <tbody><tr> - <td class="file high" data-value="src"><a href="src/index.html">src</a></td> - <td data-value="82.35" class="pic high"> - <div class="chart"><div class="cover-fill" style="width: 82%"></div><div class="cover-empty" style="width: 18%"></div></div> + <td class="file medium" data-value="src"><a href="src/index.html">src</a></td> + <td data-value="73.68" class="pic medium"> + <div class="chart"><div class="cover-fill" style="width: 73%"></div><div class="cover-empty" style="width: 27%"></div></div> </td> - <td data-value="82.35" class="pct high">82.35%</td> - <td data-value="17" class="abs high">14/17</td> + <td data-value="73.68" class="pct medium">73.68%</td> + <td data-value="19" class="abs medium">14/19</td> <td data-value="66.66" class="pct medium">66.66%</td> <td data-value="6" class="abs medium">4/6</td> - <td data-value="87.5" class="pct high">87.5%</td> - <td data-value="8" class="abs high">7/8</td> - <td data-value="82.35" class="pct high">82.35%</td> - <td data-value="17" class="abs high">14/17</td> + <td data-value="77.77" class="pct medium">77.77%</td> + <td data-value="9" class="abs medium">7/9</td> + <td data-value="73.68" class="pct medium">73.68%</td> + <td data-value="19" class="abs medium">14/19</td> </tr> <tr> <td class="file high" data-value="src/components"><a href="src/components/index.html">src/components</a></td> - <td data-value="93.42" class="pic high"> - <div class="chart"><div class="cover-fill" style="width: 93%"></div><div class="cover-empty" style="width: 7%"></div></div> + <td data-value="86.77" 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="93.42" class="pct high">93.42%</td> - <td data-value="76" class="abs high">71/76</td> - <td data-value="82.69" class="pct high">82.69%</td> - <td data-value="52" class="abs high">43/52</td> - <td data-value="95.45" class="pct high">95.45%</td> - <td data-value="22" class="abs high">21/22</td> - <td data-value="93.05" class="pct high">93.05%</td> - <td data-value="72" class="abs high">67/72</td> + <td data-value="86.77" class="pct high">86.77%</td> + <td data-value="121" class="abs high">105/121</td> + <td data-value="76.47" class="pct medium">76.47%</td> + <td data-value="68" class="abs medium">52/68</td> + <td data-value="89.74" class="pct high">89.74%</td> + <td data-value="39" class="abs high">35/39</td> + <td data-value="90.09" class="pct high">90.09%</td> + <td data-value="111" class="abs high">100/111</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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="prettify.js"></script> <script> diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html index d927374..a833e51 100644 --- a/frontend/coverage/src/App.css.html +++ b/frontend/coverage/src/App.css.html @@ -295,7 +295,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/coverage/src/App.tsx.html b/frontend/coverage/src/App.tsx.html index cdf0b6b..956b16a 100644 --- a/frontend/coverage/src/App.tsx.html +++ b/frontend/coverage/src/App.tsx.html @@ -23,9 +23,9 @@ <div class='clearfix'> <div class='fl pad1y space-right2'> - <span class="strong">82.35% </span> + <span class="strong">73.68% </span> <span class="quiet">Statements</span> - <span class='fraction'>14/17</span> + <span class='fraction'>14/19</span> </div> @@ -37,16 +37,16 @@ <div class='fl pad1y space-right2'> - <span class="strong">87.5% </span> + <span class="strong">77.77% </span> <span class="quiet">Functions</span> - <span class='fraction'>7/8</span> + <span class='fraction'>7/9</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">82.35% </span> + <span class="strong">73.68% </span> <span class="quiet">Lines</span> - <span class='fraction'>14/17</span> + <span class='fraction'>14/19</span> </div> @@ -61,7 +61,7 @@ </div> </template> </div> - <div class='status-line high'></div> + <div class='status-line medium'></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> @@ -147,7 +147,19 @@ <a name='L82'></a><a href='#L82'>82</a> <a name='L83'></a><a href='#L83'>83</a> <a name='L84'></a><a href='#L84'>84</a> -<a name='L85'></a><a href='#L85'>85</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<a name='L85'></a><a href='#L85'>85</a> +<a name='L86'></a><a href='#L86'>86</a> +<a name='L87'></a><a href='#L87'>87</a> +<a name='L88'></a><a href='#L88'>88</a> +<a name='L89'></a><a href='#L89'>89</a> +<a name='L90'></a><a href='#L90'>90</a> +<a name='L91'></a><a href='#L91'>91</a> +<a name='L92'></a><a href='#L92'>92</a> +<a name='L93'></a><a href='#L93'>93</a> +<a name='L94'></a><a href='#L94'>94</a> +<a name='L95'></a><a href='#L95'>95</a> +<a name='L96'></a><a href='#L96'>96</a> +<a name='L97'></a><a href='#L97'>97</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> @@ -184,12 +196,23 @@ <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> <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-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-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">1x</span> <span class="cline-any cline-yes">1x</span> <span class="cline-any cline-neutral"> </span> @@ -213,6 +236,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">2x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -266,6 +290,7 @@ function RequireAuth({ children }: { children: React.ReactElement }) { import FeedList from './components/FeedList'; import FeedItems from './components/FeedItems'; +import Settings from './components/Settings'; function Dashboard() { return ( @@ -273,6 +298,16 @@ function Dashboard() { <header className="dashboard-header"> <h1>Neko Reader</h1> <nav> + <a href="/settings" onClick={<span class="fstat-no" title="function not covered" >(e</span>) => { +<span class="cstat-no" title="statement not covered" > e.preventDefault();</span> +<span class="cstat-no" title="statement not covered" > window.history.pushState({}, '', '/settings');</span> + // Quick hack for navigation without full router link if inside Router context, + // but here we are inside BrowserRouter so we should use Link or just simple navigation + // actually let's just use a real Link if we can, but we need import. + // For now, let's just rely on the Router catching the URL change if we use proper Link + // or just a button that navigates. + }} style={{ color: 'white', marginRight: '1rem' }}>Settings</a> + <button onClick={() => { fetch('/api/logout', { method: 'POST' }) .then(() => window.location.href = '/login/'); @@ -288,6 +323,7 @@ function Dashboard() { <main className="dashboard-main"> <Routes> <Route path="/feed/:feedId" element={<FeedItems />} /> + <Route path="/settings" element={<Settings />} /> <Route path="/" element={<p>Select a feed to view items.</p>} /> </Routes> </main> @@ -322,7 +358,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedItem.css.html b/frontend/coverage/src/components/FeedItem.css.html index 7cd7331..aa1f94f 100644 --- a/frontend/coverage/src/components/FeedItem.css.html +++ b/frontend/coverage/src/components/FeedItem.css.html @@ -310,7 +310,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedItem.tsx.html b/frontend/coverage/src/components/FeedItem.tsx.html index 9f34545..893040e 100644 --- a/frontend/coverage/src/components/FeedItem.tsx.html +++ b/frontend/coverage/src/components/FeedItem.tsx.html @@ -337,7 +337,7 @@ export default function FeedItem({ item: initialItem }: FeedItemProps) { <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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </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 index 7cdb076..826334f 100644 --- a/frontend/coverage/src/components/FeedItems.css.html +++ b/frontend/coverage/src/components/FeedItems.css.html @@ -109,7 +109,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html index 5ff592e..462491f 100644 --- a/frontend/coverage/src/components/FeedItems.tsx.html +++ b/frontend/coverage/src/components/FeedItems.tsx.html @@ -238,7 +238,7 @@ export default function FeedItems() { <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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/FeedList.css.html b/frontend/coverage/src/components/FeedList.css.html index e44fb5c..b2087e7 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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </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 c858d56..75aeb6a 100644 --- a/frontend/coverage/src/components/FeedList.tsx.html +++ b/frontend/coverage/src/components/FeedList.tsx.html @@ -223,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </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 b828e75..7a0a19e 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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </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 ef24b05..cc6a099 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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/components/Settings.css.html b/frontend/coverage/src/components/Settings.css.html new file mode 100644 index 0000000..5bd4b53 --- /dev/null +++ b/frontend/coverage/src/components/Settings.css.html @@ -0,0 +1,331 @@ + +<!doctype html> +<html lang="en"> + +<head> + <title>Code coverage report for src/components/Settings.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> Settings.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> +<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> +<a name='L68'></a><a href='#L68'>68</a> +<a name='L69'></a><a href='#L69'>69</a> +<a name='L70'></a><a href='#L70'>70</a> +<a name='L71'></a><a href='#L71'>71</a> +<a name='L72'></a><a href='#L72'>72</a> +<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> +<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> +<a name='L81'></a><a href='#L81'>81</a> +<a name='L82'></a><a href='#L82'>82</a> +<a name='L83'></a><a href='#L83'>83</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> +<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">.settings-page { + padding: 2rem; + max-width: 800px; + margin: 0 auto; +} + +.add-feed-section { + background: #f9f9f9; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border: 1px solid #eee; +} + +.add-feed-form { + display: flex; + gap: 1rem; +} + +.feed-input { + flex: 1; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; +} + +.error-message { + color: #d32f2f; + margin-top: 1rem; +} + +.settings-feed-list { + list-style: none; + padding: 0; + border: 1px solid #eee; + border-radius: 8px; +} + +.settings-feed-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #eee; +} + +.settings-feed-item:last-child { + border-bottom: none; +} + +.feed-info { + display: flex; + flex-direction: column; +} + +.feed-title { + font-weight: bold; + font-size: 1.1rem; +} + +.feed-url { + color: #666; + font-size: 0.9rem; +} + +.delete-btn { + background: #ff5252; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; +} + +.delete-btn:hover { + background: #ff1744; +} + +.delete-btn:disabled { + background: #ffcdd2; + cursor: not-allowed; +}</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-13T15:03:42.999Z + </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/Settings.tsx.html b/frontend/coverage/src/components/Settings.tsx.html new file mode 100644 index 0000000..eaed089 --- /dev/null +++ b/frontend/coverage/src/components/Settings.tsx.html @@ -0,0 +1,448 @@ + +<!doctype html> +<html lang="en"> + +<head> + <title>Code coverage report for src/components/Settings.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> Settings.tsx</h1> + <div class='clearfix'> + + <div class='fl pad1y space-right2'> + <span class="strong">75.55% </span> + <span class="quiet">Statements</span> + <span class='fraction'>34/45</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">56.25% </span> + <span class="quiet">Branches</span> + <span class='fraction'>9/16</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">82.35% </span> + <span class="quiet">Functions</span> + <span class='fraction'>14/17</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">84.61% </span> + <span class="quiet">Lines</span> + <span class='fraction'>33/39</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 medium'></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> +<a name='L68'></a><a href='#L68'>68</a> +<a name='L69'></a><a href='#L69'>69</a> +<a name='L70'></a><a href='#L70'>70</a> +<a name='L71'></a><a href='#L71'>71</a> +<a name='L72'></a><a href='#L72'>72</a> +<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> +<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> +<a name='L81'></a><a href='#L81'>81</a> +<a name='L82'></a><a href='#L82'>82</a> +<a name='L83'></a><a href='#L83'>83</a> +<a name='L84'></a><a href='#L84'>84</a> +<a name='L85'></a><a href='#L85'>85</a> +<a name='L86'></a><a href='#L86'>86</a> +<a name='L87'></a><a href='#L87'>87</a> +<a name='L88'></a><a href='#L88'>88</a> +<a name='L89'></a><a href='#L89'>89</a> +<a name='L90'></a><a href='#L90'>90</a> +<a name='L91'></a><a href='#L91'>91</a> +<a name='L92'></a><a href='#L92'>92</a> +<a name='L93'></a><a href='#L93'>93</a> +<a name='L94'></a><a href='#L94'>94</a> +<a name='L95'></a><a href='#L95'>95</a> +<a name='L96'></a><a href='#L96'>96</a> +<a name='L97'></a><a href='#L97'>97</a> +<a name='L98'></a><a href='#L98'>98</a> +<a name='L99'></a><a href='#L99'>99</a> +<a name='L100'></a><a href='#L100'>100</a> +<a name='L101'></a><a href='#L101'>101</a> +<a name='L102'></a><a href='#L102'>102</a> +<a name='L103'></a><a href='#L103'>103</a> +<a name='L104'></a><a href='#L104'>104</a> +<a name='L105'></a><a href='#L105'>105</a> +<a name='L106'></a><a href='#L106'>106</a> +<a name='L107'></a><a href='#L107'>107</a> +<a name='L108'></a><a href='#L108'>108</a> +<a name='L109'></a><a href='#L109'>109</a> +<a name='L110'></a><a href='#L110'>110</a> +<a name='L111'></a><a href='#L111'>111</a> +<a name='L112'></a><a href='#L112'>112</a> +<a name='L113'></a><a href='#L113'>113</a> +<a name='L114'></a><a href='#L114'>114</a> +<a name='L115'></a><a href='#L115'>115</a> +<a name='L116'></a><a href='#L116'>116</a> +<a name='L117'></a><a href='#L117'>117</a> +<a name='L118'></a><a href='#L118'>118</a> +<a name='L119'></a><a href='#L119'>119</a> +<a name='L120'></a><a href='#L120'>120</a> +<a name='L121'></a><a href='#L121'>121</a> +<a name='L122'></a><a href='#L122'>122</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">14x</span> +<span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-yes">4x</span> +<span class="cline-any cline-yes">4x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">4x</span> +<span class="cline-any cline-yes">4x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">4x</span> +<span class="cline-any cline-yes">4x</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">14x</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-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-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-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">14x</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-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-yes">1x</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">14x</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-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-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-yes">5x</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">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-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 React, { useEffect, useState } from 'react'; +import type { Feed } from '../types'; +import './Settings.css'; + +export default function Settings() { + const [feeds, setFeeds] = useState<Feed[]>([]); + const [newFeedUrl, setNewFeedUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + fetchFeeds(); + }, []); + + const fetchFeeds = () => { + setLoading(true); + fetch('/api/feed/') + .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 feeds');</span> + return res.json(); + }) + .then((data) => { + setFeeds(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> + }); + }; + + const handleAddFeed = (e: React.FormEvent) => { + e.preventDefault(); + <span class="missing-if-branch" title="if path not taken" >I</span>if (!newFeedUrl) <span class="cstat-no" title="statement not covered" >return;</span> + + setLoading(true); + fetch('/api/feed/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: newFeedUrl }), + }) + .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 add feed');</span> + return res.json(); + }) + .then(() => { + setNewFeedUrl(''); + fetchFeeds(); // Refresh list (or we could append if server returns full feed object) + }) + .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> + }); + }; + + const handleDeleteFeed = (id: number) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!globalThis.confirm('Are you sure you want to delete this feed?')) <span class="cstat-no" title="statement not covered" >return;</span> + + setLoading(true); + fetch(`/api/feed/${id}`, { + method: 'DELETE', + }) + .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 delete feed');</span> + setFeeds(feeds.filter((f) => f._id !== id)); + 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> + }); + }; + + return ( + <div className="settings-page"> + <h2>Settings</h2> + + <div className="add-feed-section"> + <h3>Add New Feed</h3> + <form onSubmit={handleAddFeed} className="add-feed-form"> + <input + type="url" + value={newFeedUrl} + onChange={(e) => setNewFeedUrl(e.target.value)} + placeholder="https://example.com/feed.xml" + required + className="feed-input" + disabled={loading} + /> + <button type="submit" disabled={loading}> + Add Feed + </button> + </form> + {error && <span class="branch-1 cbranch-no" title="branch not covered" ><p className="error-message">{error}</p>}</span> + </div> + + <div className="feed-list-section"> + <h3>Manage Feeds</h3> + {loading && <p>Loading...</p>} + <ul className="settings-feed-list"> + {feeds.map((feed) => ( + <li key={feed._id} className="settings-feed-item"> + <div className="feed-info"> + <span className="feed-title">{feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}<</span>/span> + <span className="feed-url">{feed.url}</span> + </div> + <button + onClick={() => handleDeleteFeed(feed._id)} + className="delete-btn" + disabled={loading} + title="Delete Feed" + > + Delete + </button> + </li> + ))} + </ul> + </div> + </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-13T15:03:42.999Z + </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/index.html b/frontend/coverage/src/components/index.html index 6af23d0..ec5db40 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">93.42% </span> + <span class="strong">86.77% </span> <span class="quiet">Statements</span> - <span class='fraction'>71/76</span> + <span class='fraction'>105/121</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">82.69% </span> + <span class="strong">76.47% </span> <span class="quiet">Branches</span> - <span class='fraction'>43/52</span> + <span class='fraction'>52/68</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">95.45% </span> + <span class="strong">89.74% </span> <span class="quiet">Functions</span> - <span class='fraction'>21/22</span> + <span class='fraction'>35/39</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">93.05% </span> + <span class="strong">90.09% </span> <span class="quiet">Lines</span> - <span class='fraction'>67/72</span> + <span class='fraction'>100/111</span> </div> @@ -198,6 +198,36 @@ <td data-value="17" class="abs high">17/17</td> </tr> +<tr> + <td class="file empty" data-value="Settings.css"><a href="Settings.css.html">Settings.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 medium" data-value="Settings.tsx"><a href="Settings.tsx.html">Settings.tsx</a></td> + <td data-value="75.55" class="pic medium"> + <div class="chart"><div class="cover-fill" style="width: 75%"></div><div class="cover-empty" style="width: 25%"></div></div> + </td> + <td data-value="75.55" class="pct medium">75.55%</td> + <td data-value="45" class="abs medium">34/45</td> + <td data-value="56.25" class="pct medium">56.25%</td> + <td data-value="16" class="abs medium">9/16</td> + <td data-value="82.35" class="pct high">82.35%</td> + <td data-value="17" class="abs high">14/17</td> + <td data-value="84.61" class="pct high">84.61%</td> + <td data-value="39" class="abs high">33/39</td> + </tr> + </tbody> </table> </div> @@ -206,7 +236,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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../../prettify.js"></script> <script> diff --git a/frontend/coverage/src/index.html b/frontend/coverage/src/index.html index dc2e167..5f85c77 100644 --- a/frontend/coverage/src/index.html +++ b/frontend/coverage/src/index.html @@ -23,9 +23,9 @@ <div class='clearfix'> <div class='fl pad1y space-right2'> - <span class="strong">82.35% </span> + <span class="strong">73.68% </span> <span class="quiet">Statements</span> - <span class='fraction'>14/17</span> + <span class='fraction'>14/19</span> </div> @@ -37,16 +37,16 @@ <div class='fl pad1y space-right2'> - <span class="strong">87.5% </span> + <span class="strong">77.77% </span> <span class="quiet">Functions</span> - <span class='fraction'>7/8</span> + <span class='fraction'>7/9</span> </div> <div class='fl pad1y space-right2'> - <span class="strong">82.35% </span> + <span class="strong">73.68% </span> <span class="quiet">Lines</span> - <span class='fraction'>14/17</span> + <span class='fraction'>14/19</span> </div> @@ -61,7 +61,7 @@ </div> </template> </div> - <div class='status-line high'></div> + <div class='status-line medium'></div> <div class="pad1"> <table class="coverage-summary"> <thead> @@ -94,18 +94,18 @@ </tr> <tr> - <td class="file high" data-value="App.tsx"><a href="App.tsx.html">App.tsx</a></td> - <td data-value="82.35" class="pic high"> - <div class="chart"><div class="cover-fill" style="width: 82%"></div><div class="cover-empty" style="width: 18%"></div></div> + <td class="file medium" data-value="App.tsx"><a href="App.tsx.html">App.tsx</a></td> + <td data-value="73.68" class="pic medium"> + <div class="chart"><div class="cover-fill" style="width: 73%"></div><div class="cover-empty" style="width: 27%"></div></div> </td> - <td data-value="82.35" class="pct high">82.35%</td> - <td data-value="17" class="abs high">14/17</td> + <td data-value="73.68" class="pct medium">73.68%</td> + <td data-value="19" class="abs medium">14/19</td> <td data-value="66.66" class="pct medium">66.66%</td> <td data-value="6" class="abs medium">4/6</td> - <td data-value="87.5" class="pct high">87.5%</td> - <td data-value="8" class="abs high">7/8</td> - <td data-value="82.35" class="pct high">82.35%</td> - <td data-value="17" class="abs high">14/17</td> + <td data-value="77.77" class="pct medium">77.77%</td> + <td data-value="9" class="abs medium">7/9</td> + <td data-value="73.68" class="pct medium">73.68%</td> + <td data-value="19" class="abs medium">14/19</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-13T15:01:10.781Z + at 2026-02-13T15:03:42.999Z </div> <script src="../prettify.js"></script> <script> diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7c9d555..8c7be19 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,7 @@ function RequireAuth({ children }: { children: React.ReactElement }) { import FeedList from './components/FeedList'; import FeedItems from './components/FeedItems'; +import Settings from './components/Settings'; function Dashboard() { return ( @@ -40,6 +41,16 @@ function Dashboard() { <header className="dashboard-header"> <h1>Neko Reader</h1> <nav> + <a href="/settings" onClick={(e) => { + e.preventDefault(); + window.history.pushState({}, '', '/settings'); + // Quick hack for navigation without full router link if inside Router context, + // but here we are inside BrowserRouter so we should use Link or just simple navigation + // actually let's just use a real Link if we can, but we need import. + // For now, let's just rely on the Router catching the URL change if we use proper Link + // or just a button that navigates. + }} style={{ color: 'white', marginRight: '1rem' }}>Settings</a> + <button onClick={() => { fetch('/api/logout', { method: 'POST' }) .then(() => window.location.href = '/login/'); @@ -55,6 +66,7 @@ function Dashboard() { <main className="dashboard-main"> <Routes> <Route path="/feed/:feedId" element={<FeedItems />} /> + <Route path="/settings" element={<Settings />} /> <Route path="/" element={<p>Select a feed to view items.</p>} /> </Routes> </main> diff --git a/frontend/src/components/Settings.css b/frontend/src/components/Settings.css new file mode 100644 index 0000000..4065e88 --- /dev/null +++ b/frontend/src/components/Settings.css @@ -0,0 +1,83 @@ +.settings-page { + padding: 2rem; + max-width: 800px; + margin: 0 auto; +} + +.add-feed-section { + background: #f9f9f9; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border: 1px solid #eee; +} + +.add-feed-form { + display: flex; + gap: 1rem; +} + +.feed-input { + flex: 1; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; +} + +.error-message { + color: #d32f2f; + margin-top: 1rem; +} + +.settings-feed-list { + list-style: none; + padding: 0; + border: 1px solid #eee; + border-radius: 8px; +} + +.settings-feed-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #eee; +} + +.settings-feed-item:last-child { + border-bottom: none; +} + +.feed-info { + display: flex; + flex-direction: column; +} + +.feed-title { + font-weight: bold; + font-size: 1.1rem; +} + +.feed-url { + color: #666; + font-size: 0.9rem; +} + +.delete-btn { + background: #ff5252; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; +} + +.delete-btn:hover { + background: #ff1744; +} + +.delete-btn:disabled { + background: #ffcdd2; + cursor: not-allowed; +}
\ No newline at end of file diff --git a/frontend/src/components/Settings.test.tsx b/frontend/src/components/Settings.test.tsx new file mode 100644 index 0000000..a15192d --- /dev/null +++ b/frontend/src/components/Settings.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import Settings from './Settings'; + +describe('Settings Component', () => { + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + // Mock confirm + global.confirm = vi.fn(() => true); + }); + + it('renders feed list', async () => { + const mockFeeds = [ + { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, + { _id: 2, title: 'Gaming', url: 'http://gaming.com/rss', category: 'gaming' }, + ]; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockFeeds, + }); + + render(<Settings />); + + await waitFor(() => { + expect(screen.getByText('Tech News')).toBeInTheDocument(); + expect(screen.getByText('http://tech.com/rss')).toBeInTheDocument(); + expect(screen.getByText('Gaming')).toBeInTheDocument(); + }); + }); + + it('adds a new feed', async () => { + (global.fetch as any) + .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({}) }) // Add feed + .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 3, title: 'New Feed', url: 'http://new.com/rss' }] }); // Refresh load + + render(<Settings />); + + // Wait for initial load to finish + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText('https://example.com/feed.xml'); + const button = screen.getByText('Add Feed'); + + fireEvent.change(input, { target: { value: 'http://new.com/rss' } }); + fireEvent.click(button); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('/api/feed/', expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ url: 'http://new.com/rss' }), + })); + }); + + // Wait for refresh + await waitFor(() => { + expect(screen.getByText('New Feed')).toBeInTheDocument(); + }); + }); + + it('deletes a feed', async () => { + const mockFeeds = [ + { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, + ]; + + (global.fetch as any) + .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds }) // Initial load + .mockResolvedValueOnce({ ok: true }); // Delete + + render(<Settings />); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + expect(screen.getByText('Tech News')).toBeInTheDocument(); + }); + + const deleteBtn = screen.getByTitle('Delete Feed'); + fireEvent.click(deleteBtn); + + await waitFor(() => { + expect(global.confirm).toHaveBeenCalled(); + expect(global.fetch).toHaveBeenCalledWith('/api/feed/1', expect.objectContaining({ method: 'DELETE' })); + expect(screen.queryByText('Tech News')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx new file mode 100644 index 0000000..def8ffe --- /dev/null +++ b/frontend/src/components/Settings.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useState } from 'react'; +import type { Feed } from '../types'; +import './Settings.css'; + +export default function Settings() { + const [feeds, setFeeds] = useState<Feed[]>([]); + const [newFeedUrl, setNewFeedUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + fetchFeeds(); + }, []); + + const fetchFeeds = () => { + setLoading(true); + fetch('/api/feed/') + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch feeds'); + return res.json(); + }) + .then((data) => { + setFeeds(data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; + + const handleAddFeed = (e: React.FormEvent) => { + e.preventDefault(); + if (!newFeedUrl) return; + + setLoading(true); + fetch('/api/feed/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: newFeedUrl }), + }) + .then((res) => { + if (!res.ok) throw new Error('Failed to add feed'); + return res.json(); + }) + .then(() => { + setNewFeedUrl(''); + fetchFeeds(); // Refresh list (or we could append if server returns full feed object) + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; + + const handleDeleteFeed = (id: number) => { + if (!globalThis.confirm('Are you sure you want to delete this feed?')) return; + + setLoading(true); + fetch(`/api/feed/${id}`, { + method: 'DELETE', + }) + .then((res) => { + if (!res.ok) throw new Error('Failed to delete feed'); + setFeeds(feeds.filter((f) => f._id !== id)); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; + + return ( + <div className="settings-page"> + <h2>Settings</h2> + + <div className="add-feed-section"> + <h3>Add New Feed</h3> + <form onSubmit={handleAddFeed} className="add-feed-form"> + <input + type="url" + value={newFeedUrl} + onChange={(e) => setNewFeedUrl(e.target.value)} + placeholder="https://example.com/feed.xml" + required + className="feed-input" + disabled={loading} + /> + <button type="submit" disabled={loading}> + Add Feed + </button> + </form> + {error && <p className="error-message">{error}</p>} + </div> + + <div className="feed-list-section"> + <h3>Manage Feeds</h3> + {loading && <p>Loading...</p>} + <ul className="settings-feed-list"> + {feeds.map((feed) => ( + <li key={feed._id} className="settings-feed-item"> + <div className="feed-info"> + <span className="feed-title">{feed.title || '(No Title)'}</span> + <span className="feed-url">{feed.url}</span> + </div> + <button + onClick={() => handleDeleteFeed(feed._id)} + className="delete-btn" + disabled={loading} + title="Delete Feed" + > + Delete + </button> + </li> + ))} + </ul> + </div> + </div> + ); +} |
