aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/coverage/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/coverage/src')
-rw-r--r--frontend/coverage/src/App.css.html273
-rw-r--r--frontend/coverage/src/App.tsx.html205
-rw-r--r--frontend/coverage/src/components/FeedItem.css.html298
-rw-r--r--frontend/coverage/src/components/FeedItem.tsx.html380
-rw-r--r--frontend/coverage/src/components/FeedItems.css.html183
-rw-r--r--frontend/coverage/src/components/FeedItems.tsx.html597
-rw-r--r--frontend/coverage/src/components/FeedList.css.html495
-rw-r--r--frontend/coverage/src/components/FeedList.tsx.html620
-rw-r--r--frontend/coverage/src/components/Login.css.html137
-rw-r--r--frontend/coverage/src/components/Login.tsx.html225
-rw-r--r--frontend/coverage/src/components/Settings.css.html453
-rw-r--r--frontend/coverage/src/components/Settings.tsx.html524
-rw-r--r--frontend/coverage/src/components/index.html504
-rw-r--r--frontend/coverage/src/index.html251
14 files changed, 3081 insertions, 2064 deletions
diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html
index a5aa753..5fca2a7 100644
--- a/frontend/coverage/src/App.css.html
+++ b/frontend/coverage/src/App.css.html
@@ -1,61 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/App.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 type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">src</a> App.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 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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -128,69 +135,7 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L73'></a><a href='#L73'>73</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -269,8 +214,6 @@
&nbsp;
body {
margin: 0;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
}
&nbsp;
/* Dashboard Layout */
@@ -282,60 +225,28 @@ body {
/* Prevent body scroll */
}
&nbsp;
-.dashboard-header {
- /* Legacy didn't really have a top header, but we need one for settings/logout.
- Keeping it minimal/flat or matching sidebar bg if we want to blend in.
- For now, let's make it dark gray to stand out less or match legacy dark mode if applicable.
- Actually, let's keep it distinct but apply the font styles. */
- background: #222;
- color: white;
- padding: 0.5rem 1rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-weight: bold;
-}
-&nbsp;
-.dashboard-header h1 {
- margin: 0;
- font-size: 1.2rem;
- font-variant: small-caps;
- text-transform: lowercase;
-}
-&nbsp;
-.nav-link,
-.logout-btn {
- font-weight: bold;
- font-variant: small-caps;
- text-transform: lowercase;
- font-size: 1rem;
- background: transparent;
- border: none;
- color: #ccc;
- cursor: pointer;
- margin-left: 1rem;
-}
-&nbsp;
-.nav-link:hover,
-.logout-btn:hover {
- color: white;
- text-decoration: underline;
-}
+/* Header styles removed as we moved to sidebar navigation */
&nbsp;
.dashboard-content {
display: flex;
flex: 1;
overflow: hidden;
+ position: relative;
}
&nbsp;
.dashboard-sidebar {
- width: 15rem;
+ width: 11rem;
background: var(--sidebar-bg);
- border-right: 1px solid #999;
+ border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow-y: auto;
- padding: 1rem;
+ transition: margin-left 0.4s ease;
+ /* No padding here, handled in FeedList */
+}
+&nbsp;
+.dashboard-sidebar.hidden {
+ margin-left: -11rem;
}
&nbsp;
.dashboard-main {
@@ -347,42 +258,44 @@ body {
}
&nbsp;
.dashboard-main&gt;* {
- max-width: 600px;
- margin: 0;
+ max-width: 35em;
+ margin: 0 auto;
}
&nbsp;
-.logout-btn {
+.fixed-toggle {
+ position: absolute;
+ top: 1rem;
+ left: 1rem;
+ z-index: 1000;
background: transparent;
- border: 1px solid rgba(255, 255, 255, 0.3);
- color: white;
- padding: 0.5rem 1rem;
- border-radius: 4px;
+ border: none;
+ font-size: 2rem;
+ line-height: 1;
cursor: pointer;
- transition: all 0.2s;
- font-size: 0.9rem;
+ padding: 0;
+ color: var(--text-color);
+ /* Inherit didn't work well if parent is transparent */
}
&nbsp;
-.logout-btn:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.5);
+.fixed-toggle:hover {
+ transform: scale(1.1);
}</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-13T21:49:58.924Z
- </div>
- <script src="../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../sorter.js"></script>
- <script src="../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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/App.tsx.html b/frontend/coverage/src/App.tsx.html
index ed02366..cd305cc 100644
--- a/frontend/coverage/src/App.tsx.html
+++ b/frontend/coverage/src/App.tsx.html
@@ -1,61 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/App.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 type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">src</a> App.tsx</h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">15/19</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">66.66% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">4/6</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">77.77% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">7/9</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">15/19</span>
- </div>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">72.72% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>16/22</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">62.5% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>10/16</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">62.5% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>5/8</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">72.72% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>16/22</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line medium"></div>
- <pre><table class="coverage">
+ </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>
@@ -146,7 +153,17 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -185,16 +202,18 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">1x</span>
-<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -215,8 +234,14 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -237,9 +262,10 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import React, { useEffect, useState } from 'react';
-import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
+import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import Login from './components/Login';
import './App.css';
+import { apiFetch } from './utils';
&nbsp;
// Protected Route wrapper
function RequireAuth({ children }: { children: React.ReactElement }) {
@@ -247,7 +273,7 @@ function RequireAuth({ children }: { children: React.ReactElement }) {
const location = useLocation();
&nbsp;
useEffect(() =&gt; {
- fetch('/api/auth')
+ apiFetch('/api/auth')
.then((res) =&gt; {
if (res.ok) {
setAuth(true);
@@ -273,26 +299,25 @@ import FeedList from './components/FeedList';
import FeedItems from './components/FeedItems';
import Settings from './components/Settings';
&nbsp;
-function Dashboard() {
- const navigate = useNavigate();
- return (
- &lt;div className="dashboard"&gt;
- &lt;header className="dashboard-header"&gt;
- &lt;h1&gt;Neko Reader&lt;/h1&gt;
- &lt;nav&gt;
- &lt;button onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >n</span>avigate('/settings')} c</span>lassName="nav-link" style={{ color: 'white', marginRight: '1rem', background: 'none', border: 'none', cursor: 'pointer', fontSize: 'inherit', fontFamily: 'inherit' }}&gt;Settings&lt;/button&gt;
+function Dashboard({ theme, setTheme }: { theme: string; setTheme: (t: string) =&gt; void }) {
+ const [sidebarVisible, setSidebarVisible] = useState(true);
&nbsp;
- &lt;button onClick={() =&gt; {
- fetch('/api/logout', { method: 'POST' })
- .then(() =&gt; window.location.href = '/v2/login');
- }} className="logout-btn"&gt;
- Logout
- &lt;/button&gt;
- &lt;/nav&gt;
- &lt;/header&gt;
+ return (
+ &lt;div
+ className={`dashboard ${sidebarVisible ? 'sidebar-visible' : <span class="branch-1 cbranch-no" title="branch not covered" >'sidebar-hidden'}</span> theme-${theme}`}
+ &gt;
&lt;div className="dashboard-content"&gt;
- &lt;aside className="dashboard-sidebar"&gt;
- &lt;FeedList /&gt;
+ {!sidebarVisible &amp;&amp; (
+<span class="branch-1 cbranch-no" title="branch not covered" > &lt;button</span>
+ className="sidebar-toggle fixed-toggle"
+ onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etSidebarVisible(true)}</span>
+ title="Show Sidebar"
+ &gt;
+ 🐱
+ &lt;/button&gt;
+ )}
+ &lt;aside className={`dashboard-sidebar ${sidebarVisible ? '' : <span class="branch-1 cbranch-no" title="branch not covered" >'hidden'}</span>`}&gt;
+ &lt;FeedList theme={theme} setTheme={setTheme} setSidebarVisible={setSidebarVisible} /&gt;
&lt;/aside&gt;
&lt;main className="dashboard-main"&gt;
&lt;Routes&gt;
@@ -308,15 +333,24 @@ function Dashboard() {
}
&nbsp;
function App() {
+ const [theme, setTheme] = useState(localStorage.getItem('neko-theme') || 'light');
+&nbsp;
+ const handleSetTheme = <span class="fstat-no" title="function not covered" >(n</span>ewTheme: string) =&gt; {
+<span class="cstat-no" title="statement not covered" > setTheme(newTheme);</span>
+<span class="cstat-no" title="statement not covered" > localStorage.setItem('neko-theme', newTheme);</span>
+ };
+&nbsp;
+ const basename = window.location.pathname.startsWith('/v2') ? '/v2' : <span class="branch-1 cbranch-no" title="branch not covered" >'/';</span>
+&nbsp;
return (
- &lt;BrowserRouter basename="/v2"&gt;
+ &lt;BrowserRouter basename={basename}&gt;
&lt;Routes&gt;
&lt;Route path="/login" element={&lt;Login /&gt;} /&gt;
&lt;Route
path="/*"
element={
&lt;RequireAuth&gt;
- &lt;Dashboard /&gt;
+ &lt;Dashboard theme={theme} setTheme={handleSetTheme} /&gt;
&lt;/RequireAuth&gt;
}
/&gt;
@@ -328,22 +362,21 @@ function App() {
export default App;
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../sorter.js"></script>
- <script src="../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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/FeedItem.css.html b/frontend/coverage/src/components/FeedItem.css.html
index f6fe1a3..213077f 100644
--- a/frontend/coverage/src/components/FeedItem.css.html
+++ b/frontend/coverage/src/components/FeedItem.css.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedItem.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 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> FeedItem.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>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItem.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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -175,7 +179,25 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<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>
+<a name='L123'></a><a href='#L123'>123</a>
+<a name='L124'></a><a href='#L124'>124</a>
+<a name='L125'></a><a href='#L125'>125</a>
+<a name='L126'></a><a href='#L126'>126</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -292,139 +314,147 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">.feed-item {
- padding: 1rem;
- margin-top: 5rem;
- list-style: none;
- border-bottom: none;
+ padding: 1rem;
+ margin-top: 5rem;
+ list-style: none;
+ border-bottom: none;
}
&nbsp;
-.feed-item.read .item-title {
- color: #888;
- font-weight: normal;
-}
-&nbsp;
-.feed-item.unread .item-title {
- font-weight: bold;
-}
+/* removed read/unread specific font-weight to keep it always bold as requested */
&nbsp;
.item-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 0.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.5rem;
}
&nbsp;
.item-title {
- font-size: 1.25rem;
- font-weight: bold;
- /* Legacy headers were bold */
- text-decoration: none;
- color: #333;
- display: block;
- flex: 1;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 1.8rem;
+ font-weight: bold;
+ text-decoration: none;
+ color: var(--link-color);
+ display: block;
+ flex: 1;
}
&nbsp;
.item-title:hover {
- text-decoration: none;
- color: blue;
- /* Legacy link color */
+ text-decoration: none;
+ color: var(--link-color);
}
&nbsp;
.item-actions {
- display: flex;
- gap: 0.5rem;
- margin-left: 1rem;
+ display: flex;
+ gap: 0.5rem;
+ margin-left: 1rem;
}
&nbsp;
/* Legacy controls were simple text/links, but buttons are fine if minimal */
.star-btn {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1.2rem;
- padding: 0 0.5rem 0 0;
- vertical-align: middle;
- transition: color 0.2s;
- line-height: 1;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1.25rem;
+ padding: 0 0 0 0.5rem;
+ vertical-align: middle;
+ transition: color 0.2s;
+ line-height: 1;
}
&nbsp;
.star-btn.is-starred {
- color: #ffd700;
- /* Gold */
+ color: blue;
}
&nbsp;
.star-btn.is-unstarred {
- color: #ccc;
+ color: var(--text-color);
+ opacity: 0.3;
}
&nbsp;
.star-btn:hover {
- color: #ffeb3b;
+ color: blue;
}
&nbsp;
.action-btn {
- background: whitesmoke;
- border: none;
- cursor: pointer;
- padding: 2px 6px;
- font-size: 1rem;
- color: blue;
- font-weight: bold;
+ background: var(--sidebar-bg);
+ border: 1px solid var(--border-color, #ccc);
+ cursor: pointer;
+ padding: 2px 6px;
+ font-size: 1rem;
+ color: blue;
+ font-weight: bold;
}
&nbsp;
.action-btn:hover {
- background-color: #eee;
+ background-color: #eee;
}
&nbsp;
.dateline {
- margin-top: 0;
- font-weight: normal;
- font-size: .75em;
- color: #ccc;
- margin-bottom: 1rem;
+ margin-top: 0;
+ font-weight: normal;
+ font-size: 0.75em;
+ color: #ccc;
+ margin-bottom: 1rem;
}
&nbsp;
.dateline a {
- color: #ccc;
- text-decoration: none;
+ color: #ccc;
+ text-decoration: none;
}
&nbsp;
.item-description {
- color: #000;
- line-height: 1.5;
- font-size: 1rem;
- margin-top: 1rem;
+ color: var(--text-color);
+ line-height: 1.5;
+ font-size: 1rem;
+ margin-top: 1rem;
}
&nbsp;
.item-description img {
- max-width: 100%;
- height: auto;
- display: block;
- margin: 1rem 0;
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 1rem 0;
}
&nbsp;
.item-description blockquote {
- padding: 1rem 1rem 0 1rem;
- border-left: 4px solid #ddd;
- color: #666;
- margin-left: 0;
+ padding: 1rem 1rem 0 1rem;
+ border-left: 4px solid var(--sidebar-bg);
+ color: var(--text-color);
+ opacity: 0.8;
+ margin-left: 0;
+}
+&nbsp;
+.scrape-btn {
+ background: var(--bg-color);
+ border: 1px solid var(--border-color, #ccc);
+ color: blue;
+ cursor: pointer;
+ font-family: 'Helvetica Neue';
+ font-weight: bold;
+ font-size: 0.8rem;
+ padding: 2px 6px;
+ margin-left: 0.5rem;
+}
+&nbsp;
+.scrape-btn:hover {
+ background: var(--sidebar-bg);
}</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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/FeedItem.tsx.html b/frontend/coverage/src/components/FeedItem.tsx.html
index 5512b78..6e76131 100644
--- a/frontend/coverage/src/components/FeedItem.tsx.html
+++ b/frontend/coverage/src/components/FeedItem.tsx.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedItem.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 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> FeedItem.tsx
- </h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">15/19</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">88.88% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">16/18</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">85.71% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">6/7</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">15/19</span>
- </div>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItem.tsx</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">78.12% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>25/32</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">86.95% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>20/23</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">83.33% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>10/12</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">80.64% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>25/31</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line medium"></div>
- <pre><table class="coverage">
+ </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>
@@ -143,7 +147,41 @@
<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">&nbsp;</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>
+<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -151,16 +189,18 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">33x</span>
+<span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">21x</span>
-<span class="cline-any cline-yes">21x</span>
+<span class="cline-any cline-yes">33x</span>
+<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">21x</span>
+<span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">21x</span>
+<span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
@@ -196,7 +236,28 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">21x</span>
+<span class="cline-any cline-yes">33x</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">&nbsp;</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">33x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -227,108 +288,143 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react';
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from 'react';
import type { Item } from '../types';
import './FeedItem.css';
&nbsp;
+import { apiFetch } from '../utils';
+&nbsp;
interface FeedItemProps {
- item: Item;
+ item: Item;
}
&nbsp;
export default function FeedItem({ item: initialItem }: FeedItemProps) {
- const [item, setItem] = useState(initialItem);
- const [loading, setLoading] = useState(false);
+ const [item, setItem] = useState(initialItem);
+ const [loading, setLoading] = useState(false);
+&nbsp;
+ useEffect(() =&gt; {
+ setItem(initialItem);
+ }, [initialItem]);
&nbsp;
+ const toggleStar = () =&gt; {
+ updateItem({ ...item, starred: !item.starred });
+ };
&nbsp;
- const toggleStar = () =&gt; {
- updateItem({ ...item, starred: !item.starred });
- };
+ const updateItem = (newItem: Item) =&gt; {
+ setLoading(true);
+ // Optimistic update
+ const previousItem = item;
+ setItem(newItem);
&nbsp;
- const updateItem = (newItem: Item) =&gt; {
- setLoading(true);
- // Optimistic update
- const previousItem = item;
- setItem(newItem);
+ apiFetch(`/api/item/${newItem._id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ _id: newItem._id,
+ read: newItem.read,
+ starred: newItem.starred,
+ }),
+ })
+ .then((res) =&gt; {
+ <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 update item');</span>
+ }
+ return res.json();
+ })
+ .then(() =&gt; {
+ // Confirm with server response if needed, but for now we trust the optimistic update
+ // or we could setItem(updated) if the server returns the full object
+ setLoading(false);
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > console.error('Error updating item:', err);</span>
+ // Revert on error
+<span class="cstat-no" title="statement not covered" > setItem(previousItem);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
&nbsp;
- fetch(`/api/item/${newItem._id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- _id: newItem._id,
- read: newItem.read,
- starred: newItem.starred,
- }),
- })
- .then((res) =&gt; {
- <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 update item');</span>
- }
- return res.json();
- })
- .then(() =&gt; {
- // Confirm with server response if needed, but for now we trust the optimistic update
- // or we could setItem(updated) if the server returns the full object
- setLoading(false);
- })
- .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
-<span class="cstat-no" title="statement not covered" > console.error('Error updating item:', err);</span>
- // Revert on error
-<span class="cstat-no" title="statement not covered" > setItem(previousItem);</span>
-<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
- });
- };
+ const loadFullContent = (e: React.MouseEvent) =&gt; {
+ e.stopPropagation();
+ setLoading(true);
+ apiFetch(`/api/item/${item._id}`)
+ .then((res) =&gt; {
+ <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 full content');</span>
+ return res.json();
+ })
+ .then((data) =&gt; {
+ setItem({ ...item, ...data });
+ setLoading(false);
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > console.error('Error fetching full content:', err);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
&nbsp;
- return (
- &lt;li className={`feed-item ${item.read ? 'read' : 'unread'} ${loading ? 'loading' : ''}`}&gt;
- &lt;div className="item-header"&gt;
- &lt;button
- onClick={(e) =&gt; {
- e.stopPropagation();
- toggleStar();
- }}
- className={`star-btn ${item.starred ? 'is-starred' : 'is-unstarred'}`}
- title={item.starred ? "Unstar" : "Star"}
- &gt;
- {item.starred ? '★' : '☆'}
- &lt;/button&gt;
- &lt;a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title"&gt;
- {item.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}</span>
- &lt;/a&gt;
- &lt;/div&gt;
- &lt;div className="dateline"&gt;
- &lt;a href={item.url} target="_blank" rel="noopener noreferrer"&gt;
- {new Date(item.publish_date).toLocaleDateString()}
- {item.feed_title &amp;&amp; ` - ${item.feed_title}`}
- &lt;/a&gt;
- &lt;div className="item-actions" style={{ display: 'inline-block', float: 'right' }}&gt;
- &lt;/div&gt;
- &lt;/div&gt;
- {item.description &amp;&amp; (
- &lt;div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} /&gt;
- )}
- &lt;/li&gt;
- );
+ return (
+ &lt;li className={`feed-item ${item.read ? 'read' : 'unread'} ${loading ? 'loading' : ''}`}&gt;
+ &lt;div className="item-header"&gt;
+ &lt;a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title"&gt;
+ {item.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}</span>
+ &lt;/a&gt;
+ &lt;button
+ onClick={(e) =&gt; {
+ e.stopPropagation();
+ toggleStar();
+ }}
+ className={`star-btn ${item.starred ? 'is-starred' : 'is-unstarred'}`}
+ title={item.starred ? 'Unstar' : 'Star'}
+ &gt;
+ ★
+ &lt;/button&gt;
+ &lt;/div&gt;
+ &lt;div className="dateline"&gt;
+ &lt;a href={item.url} target="_blank" rel="noopener noreferrer"&gt;
+ {new Date(item.publish_date).toLocaleDateString()}
+ {item.feed_title &amp;&amp; ` - ${item.feed_title}`}
+ &lt;/a&gt;
+ &lt;div className="item-actions" style={{ display: 'inline-block', float: 'right' }}&gt;
+ {!item.full_content &amp;&amp; (
+ &lt;button onClick={loadFullContent} className="scrape-btn" title="Load Full Content"&gt;
+ text
+ &lt;/button&gt;
+ )}
+ &lt;/div&gt;
+ &lt;/div&gt;
+ {(item.full_content || item.description) &amp;&amp; (
+ &lt;div
+ className="item-description"
+ dangerouslySetInnerHTML={{ __html: item.full_content || item.description }}
+ /&gt;
+ )}
+ &lt;/li&gt;
+ );
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/FeedItems.css.html b/frontend/coverage/src/components/FeedItems.css.html
index 0b1c77d..7a971c6 100644
--- a/frontend/coverage/src/components/FeedItems.css.html
+++ b/frontend/coverage/src/components/FeedItems.css.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedItems.css</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <style type="text/css">
- .coverage-summary .sorter {
- background-image: url(../../sort-arrow-sprite.png);
- }
+ <style type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
- <h1>
- <a href="../../index.html">All files</a> /
- <a href="index.html">src/components</a> FeedItems.css
- </h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">0% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">0/0</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">0% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">0/0</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">0% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">0/0</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">0% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">0/0</span>
- </div>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItems.css</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
</div>
<p class="quiet">
- Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>,
- <em>p</em> or <em>k</em> for the previous block.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -81,23 +85,7 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L23'></a><a href='#L23'>23</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -120,53 +108,44 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">.feed-items {
- padding: 1rem;
+ padding: 1rem 0;
+ /* Removing horizontal padding to avoid double-padding with FeedItem */
}
&nbsp;
.feed-items h2 {
- margin-top: 0;
- border-bottom: 2px solid #eee;
- padding-bottom: 0.5rem;
+ margin-top: 0;
+ border-bottom: 2px solid var(--border-color);
+ padding-bottom: 0.5rem;
}
&nbsp;
.item-list {
- list-style: none;
- padding: 0;
-}
-&nbsp;
-.selected-item-container {
- border-left: 4px solid #007bff;
- background-color: #f8f9fa;
- padding-left: 0.5rem;
- margin-left: -0.5rem;
- /* Compensate for padding/border to keep alignment */
- transition: background-color 0.2s;
+ list-style: none;
+ padding: 0;
}
&nbsp;
.loading-more {
- padding: 2rem;
- text-align: center;
- color: #888;
- font-size: 0.9rem;
- min-height: 50px;
+ padding: 2rem;
+ text-align: center;
+ color: #888;
+ font-size: 0.9rem;
+ min-height: 50px;
}</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html
index e57acf9..f6b7493 100644
--- a/frontend/coverage/src/components/FeedItems.tsx.html
+++ b/frontend/coverage/src/components/FeedItems.tsx.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedItems.tsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <style type="text/css">
- .coverage-summary .sorter {
- background-image: url(../../sort-arrow-sprite.png);
- }
+ <style type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
- <h1>
- <a href="../../index.html">All files</a> /
- <a href="index.html">src/components</a> FeedItems.tsx
- </h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">89.34% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">109/122</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">77.21% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">61/79</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">86.2% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">25/29</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">89.09% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">98/110</span>
- </div>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedItems.tsx</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">88.97% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>113/127</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">75.3% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>61/81</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">86.2% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>25/29</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">88.69% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>102/115</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line high"></div>
- <pre><table class="coverage">
+ </div>
+ <div class='status-line high'></div>
+ <pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
@@ -282,7 +286,18 @@
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
-<a name='L224'></a><a href='#L224'>224</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L224'></a><a href='#L224'>224</a>
+<a name='L225'></a><a href='#L225'>225</a>
+<a name='L226'></a><a href='#L226'>226</a>
+<a name='L227'></a><a href='#L227'>227</a>
+<a name='L228'></a><a href='#L228'>228</a>
+<a name='L229'></a><a href='#L229'>229</a>
+<a name='L230'></a><a href='#L230'>230</a>
+<a name='L231'></a><a href='#L231'>231</a>
+<a name='L232'></a><a href='#L232'>232</a>
+<a name='L233'></a><a href='#L233'>233</a>
+<a name='L234'></a><a href='#L234'>234</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -298,6 +313,7 @@
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">27x</span>
+<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">8x</span>
@@ -323,6 +339,11 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
+<span class="cline-any cline-yes">8x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-no">&nbsp;</span>
@@ -330,6 +351,8 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
+<span class="cline-any cline-yes">8x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
@@ -363,77 +386,79 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">7x</span>
+<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
-<span class="cline-any cline-yes">23x</span>
-<span class="cline-any cline-yes">3x</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">3x</span>
-<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
-<span class="cline-any cline-yes">2x</span>
-<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">1x</span>
-<span class="cline-any cline-no">&nbsp;</span>
-<span class="cline-any cline-no">&nbsp;</span>
-<span class="cline-any cline-no">&nbsp;</span>
-<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</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-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">23x</span>
-<span class="cline-any cline-yes">23x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
+<span class="cline-any cline-yes">23x</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">2x</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</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-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">23x</span>
+<span class="cline-any cline-yes">23x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
@@ -477,7 +502,6 @@
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -510,242 +534,251 @@ import { useParams, useSearchParams } from 'react-router-dom';
import type { Item } from '../types';
import FeedItem from './FeedItem';
import './FeedItems.css';
+import { apiFetch } from '../utils';
&nbsp;
export default function FeedItems() {
- const { feedId, tagName } = useParams&lt;{ feedId: string; tagName: string }&gt;();
- const [searchParams] = useSearchParams();
- const filterFn = searchParams.get('filter') || 'unread';
+ const { feedId, tagName } = useParams&lt;{ feedId: string; tagName: string }&gt;();
+ const [searchParams] = useSearchParams();
+ const filterFn = searchParams.get('filter') || 'unread';
&nbsp;
- const [items, setItems] = useState&lt;Item[]&gt;([]);
- const [loading, setLoading] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
- const [hasMore, setHasMore] = useState(true);
- const [error, setError] = useState('');
+ const [items, setItems] = useState&lt;Item[]&gt;([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [hasMore, setHasMore] = useState(true);
+ const [error, setError] = useState('');
+ const [selectedIndex, setSelectedIndex] = useState(-1);
&nbsp;
- const fetchItems = (maxId?: string) =&gt; {
- if (maxId) {
- setLoadingMore(true);
- } else {
- setLoading(true);
- setItems([]);
- }
- setError('');
+ const fetchItems = (maxId?: string) =&gt; {
+ if (maxId) {
+ setLoadingMore(true);
+ } else {
+ setLoading(true);
+ setItems([]);
+ }
+ setError('');
&nbsp;
- let url = '/api/stream';
- const params = new URLSearchParams();
+ let url = '/api/stream';
+ const params = new URLSearchParams();
&nbsp;
- if (feedId) {
- params.append('feed_id', feedId);
- } else if (tagName) {
- params.append('tag', tagName);
- }
+ if (feedId) {
+ params.append('feed_id', feedId);
+ } else if (tagName) {
+ params.append('tag', tagName);
+ }
&nbsp;
- if (maxId) {
- params.append('max_id', maxId);
- }
+ if (maxId) {
+ params.append('max_id', maxId);
+ }
+&nbsp;
+ // Apply filters
+ const searchQuery = searchParams.get('q');
+ <span class="missing-if-branch" title="if path not taken" >I</span>if (searchQuery) {
+<span class="cstat-no" title="statement not covered" > params.append('q', searchQuery);</span>
+ }
+&nbsp;
+ <span class="missing-if-branch" title="if path not taken" >I</span>if (filterFn === 'all') {
+<span class="cstat-no" title="statement not covered" > params.append('read_filter', 'all');</span>
+ <span class="missing-if-branch" title="if path not taken" >I</span>} else if (filterFn === 'starred') {
+<span class="cstat-no" title="statement not covered" > params.append('starred', 'true');</span>
+<span class="cstat-no" title="statement not covered" > params.append('read_filter', 'all');</span>
+ } else {
+ // default to unread
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (!searchQuery) {
+ params.append('read_filter', 'unread');
+ }
+ }
+&nbsp;
+ const queryString = params.toString();
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (queryString) {
+ url += `?${queryString}`;
+ }
&nbsp;
- // Apply filters
- <span class="missing-if-branch" title="if path not taken" >I</span>if (filterFn === 'all') {
-<span class="cstat-no" title="statement not covered" > params.append('read_filter', 'all');</span>
- <span class="missing-if-branch" title="if path not taken" >I</span>} else if (filterFn === 'starred') {
-<span class="cstat-no" title="statement not covered" > params.append('starred', 'true');</span>
-<span class="cstat-no" title="statement not covered" > params.append('read_filter', 'all');</span>
+ apiFetch(url)
+ .then((res) =&gt; {
+ <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) {
+<span class="cstat-no" title="statement not covered" > throw new Error('Failed to fetch items');</span>
+ }
+ return res.json();
+ })
+ .then((data) =&gt; {
+ if (maxId) {
+ setItems((prev) =&gt; [...prev, ...data]);
} else {
- // default to unread
- params.append('read_filter', 'unread');
+ setItems(data);
}
+ setHasMore(data.length &gt; 0);
+ setLoading(false);
+ setLoadingMore(false);
+ })
+ .catch((err) =&gt; {
+ setError(err.message);
+ setLoading(false);
+ setLoadingMore(false);
+ });
+ };
&nbsp;
- const queryString = params.toString();
- <span class="missing-if-branch" title="else path not taken" >E</span>if (queryString) {
- url += `?${queryString}`;
- }
+ useEffect(() =&gt; {
+ fetchItems();
+ setSelectedIndex(-1);
+ }, [feedId, tagName, filterFn, searchParams]);
&nbsp;
- fetch(url)
- .then((res) =&gt; {
- <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) {
-<span class="cstat-no" title="statement not covered" > throw new Error('Failed to fetch items');</span>
- }
- return res.json();
- })
- .then((data) =&gt; {
- if (maxId) {
- setItems((prev) =&gt; [...prev, ...data]);
- } else {
- setItems(data);
- }
- setHasMore(data.length &gt; 0);
- setLoading(false);
- setLoadingMore(false);
- })
- .catch((err) =&gt; {
- setError(err.message);
- setLoading(false);
- setLoadingMore(false);
- });
- };
&nbsp;
- useEffect(() =&gt; {
- fetchItems();
- }, [feedId, tagName, filterFn]);
+ const scrollToItem = (index: number) =&gt; {
+ const element = document.getElementById(`item-${index}`);
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (element) {
+ element.scrollIntoView({ behavior: 'auto', block: 'start' });
+ }
+ };
&nbsp;
- const [selectedIndex, setSelectedIndex] = useState(-1);
+ const markAsRead = (item: Item) =&gt; {
+ const updatedItem = { ...item, read: true };
+ // Optimistic update
+ setItems((prevItems) =&gt; prevItems.map((i) =&gt; (i._id === item._id ? updatedItem : i)));
&nbsp;
- useEffect(() =&gt; {
- const handleKeyDown = (e: KeyboardEvent) =&gt; {
- <span class="missing-if-branch" title="if path not taken" >I</span>if (items.length === 0) <span class="cstat-no" title="statement not covered" >return;</span>
+ apiFetch(`/api/item/${item._id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ read: true, starred: item.starred }),
+ }).catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; <span class="cstat-no" title="statement not covered" >console.error('Failed to mark read', err))</span>;
+ };
&nbsp;
- if (e.key === 'j') {
- setSelectedIndex((prev) =&gt; {
- const nextIndex = Math.min(prev + 1, items.length - 1);
- <span class="missing-if-branch" title="else path not taken" >E</span>if (nextIndex !== prev) {
- const item = items[nextIndex];
- if (!item.read) {
- markAsRead(item);
- }
- scrollToItem(nextIndex);
- }
- return nextIndex;
- });
- <span class="missing-if-branch" title="if path not taken" >I</span>} else if (e.key === 'k') {
-<span class="cstat-no" title="statement not covered" > setSelectedIndex(<span class="fstat-no" title="function not covered" >(p</span>rev) =&gt; {</span>
- const nextIndex = <span class="cstat-no" title="statement not covered" >Math.max(prev - 1, 0);</span>
-<span class="cstat-no" title="statement not covered" > if (nextIndex !== prev) {</span>
-<span class="cstat-no" title="statement not covered" > scrollToItem(nextIndex);</span>
- }
-<span class="cstat-no" title="statement not covered" > return nextIndex;</span>
- });
- <span class="missing-if-branch" title="else path not taken" >E</span>} else if (e.key === 's') {
- setSelectedIndex((currentIndex) =&gt; {
- <span class="missing-if-branch" title="else path not taken" >E</span>if (currentIndex &gt;= 0 &amp;&amp; currentIndex &lt; items.length) {
- toggleStar(items[currentIndex]);
- }
- return currentIndex;
- });
- }
- };
+ const toggleStar = (item: Item) =&gt; {
+ const updatedItem = { ...item, starred: !item.starred };
+ // Optimistic update
+ setItems((prevItems) =&gt; prevItems.map((i) =&gt; (i._id === item._id ? updatedItem : i)));
&nbsp;
- window.addEventListener('keydown', handleKeyDown);
- return () =&gt; window.removeEventListener('keydown', handleKeyDown);
- }, [items]);
+ apiFetch(`/api/item/${item._id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ read: item.read, starred: !item.starred }),
+ }).catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; <span class="cstat-no" title="statement not covered" >console.error('Failed to toggle star', err))</span>;
+ };
&nbsp;
- const scrollToItem = (index: number) =&gt; {
- const element = document.getElementById(`item-${index}`);
- <span class="missing-if-branch" title="else path not taken" >E</span>if (element) {
- element.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- };
+ useEffect(() =&gt; {
+ const handleKeyDown = (e: KeyboardEvent) =&gt; {
+ <span class="missing-if-branch" title="if path not taken" >I</span>if (items.length === 0) <span class="cstat-no" title="statement not covered" >return;</span>
&nbsp;
- const markAsRead = (item: Item) =&gt; {
- const updatedItem = { ...item, read: true };
- // Optimistic update
- setItems((prevItems) =&gt; prevItems.map((i) =&gt; (i._id === item._id ? updatedItem : i)));
-&nbsp;
- fetch(`/api/item/${item._id}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ read: true, starred: item.starred }),
- }).catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; <span class="cstat-no" title="statement not covered" >console.error('Failed to mark read', err))</span>;
+ if (e.key === 'j') {
+ setSelectedIndex((prev) =&gt; {
+ const nextIndex = Math.min(prev + 1, items.length - 1);
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (nextIndex !== prev) {
+ const item = items[nextIndex];
+ if (!item.read) {
+ markAsRead(item);
+ }
+ scrollToItem(nextIndex);
+ }
+ return nextIndex;
+ });
+ <span class="missing-if-branch" title="if path not taken" >I</span>} else if (e.key === 'k') {
+<span class="cstat-no" title="statement not covered" > setSelectedIndex(<span class="fstat-no" title="function not covered" >(p</span>rev) =&gt; {</span>
+ const nextIndex = <span class="cstat-no" title="statement not covered" >Math.max(prev - 1, 0);</span>
+<span class="cstat-no" title="statement not covered" > if (nextIndex !== prev) {</span>
+<span class="cstat-no" title="statement not covered" > scrollToItem(nextIndex);</span>
+ }
+<span class="cstat-no" title="statement not covered" > return nextIndex;</span>
+ });
+ <span class="missing-if-branch" title="else path not taken" >E</span>} else if (e.key === 's') {
+ setSelectedIndex((currentIndex) =&gt; {
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (currentIndex &gt;= 0 &amp;&amp; currentIndex &lt; items.length) {
+ toggleStar(items[currentIndex]);
+ }
+ return currentIndex;
+ });
+ }
};
&nbsp;
- const toggleStar = (item: Item) =&gt; {
- const updatedItem = { ...item, starred: !item.starred };
- // Optimistic update
- setItems((prevItems) =&gt; prevItems.map((i) =&gt; (i._id === item._id ? updatedItem : i)));
+ window.addEventListener('keydown', handleKeyDown);
+ return () =&gt; window.removeEventListener('keydown', handleKeyDown);
+ }, [items]);
&nbsp;
- fetch(`/api/item/${item._id}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ read: item.read, starred: !item.starred }),
- }).catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; <span class="cstat-no" title="statement not covered" >console.error('Failed to toggle star', err))</span>;
- };
&nbsp;
- useEffect(() =&gt; {
- const observer = new IntersectionObserver(
- (entries) =&gt; {
- entries.forEach((entry) =&gt; {
- // Infinity scroll sentinel
- if (entry.target.id === 'load-more-sentinel') {
- <span class="missing-if-branch" title="else path not taken" >E</span>if (entry.isIntersecting &amp;&amp; !loadingMore &amp;&amp; hasMore &amp;&amp; items.length &gt; 0) {
- fetchItems(String(items[items.length - 1]._id));
- }
- return;
- }
&nbsp;
- // If item is not intersecting and is above the viewport, it's been scrolled past
- <span class="missing-if-branch" title="else path not taken" >E</span>if (!entry.isIntersecting &amp;&amp; entry.boundingClientRect.top &lt; 0) {
- const index = Number(entry.target.getAttribute('data-index'));
- <span class="missing-if-branch" title="else path not taken" >E</span>if (!isNaN(index) &amp;&amp; index &gt;= 0 &amp;&amp; index &lt; items.length) {
- const item = items[index];
- <span class="missing-if-branch" title="else path not taken" >E</span>if (!item.read) {
- markAsRead(item);
- }
- }
- }
- });
- },
- { root: null, threshold: 0 }
- );
+ useEffect(() =&gt; {
+ const observer = new IntersectionObserver(
+ (entries) =&gt; {
+ entries.forEach((entry) =&gt; {
+ // Infinity scroll sentinel
+ if (entry.target.id === 'load-more-sentinel') {
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (entry.isIntersecting &amp;&amp; !loadingMore &amp;&amp; hasMore &amp;&amp; items.length &gt; 0) {
+ fetchItems(String(items[items.length - 1]._id));
+ }
+ return;
+ }
&nbsp;
- items.forEach((_, index) =&gt; {
- const el = document.getElementById(`item-${index}`);
- <span class="missing-if-branch" title="else path not taken" >E</span>if (el) observer.observe(el);
+ // If item is not intersecting and is above the viewport, it's been scrolled past
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (!entry.isIntersecting &amp;&amp; entry.boundingClientRect.top &lt; 0) {
+ const index = Number(entry.target.getAttribute('data-index'));
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (!isNaN(index) &amp;&amp; index &gt;= 0 &amp;&amp; index &lt; items.length) {
+ const item = items[index];
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (!item.read) {
+ markAsRead(item);
+ }
+ }
+ }
});
+ },
+ { root: null, threshold: 0 }
+ );
&nbsp;
- const sentinel = document.getElementById('load-more-sentinel');
- if (sentinel) observer.observe(sentinel);
+ items.forEach((_, index) =&gt; {
+ const el = document.getElementById(`item-${index}`);
+ <span class="missing-if-branch" title="else path not taken" >E</span>if (el) observer.observe(el);
+ });
&nbsp;
- return () =&gt; observer.disconnect();
- }, [items, loadingMore, hasMore]);
+ const sentinel = document.getElementById('load-more-sentinel');
+ if (sentinel) observer.observe(sentinel);
&nbsp;
- if (loading) return &lt;div className="feed-items-loading"&gt;Loading items...&lt;/div&gt;;
- if (error) return &lt;div className="feed-items-error"&gt;Error: {error}&lt;/div&gt;;
+ return () =&gt; observer.disconnect();
+ }, [items, loadingMore, hasMore]);
&nbsp;
+ if (loading) return &lt;div className="feed-items-loading"&gt;Loading items...&lt;/div&gt;;
+ if (error) return &lt;div className="feed-items-error"&gt;Error: {error}&lt;/div&gt;;
&nbsp;
- return (
- &lt;div className="feed-items"&gt;
- {items.length === 0 ? (
-<span class="branch-0 cbranch-no" title="branch not covered" > &lt;p&gt;No items found.&lt;/p&gt;</span>
- ) : (
- &lt;ul className="item-list"&gt;
- {items.map((item, index) =&gt; (
- &lt;div
- id={`item-${index}`}
- key={item._id}
- data-index={index}
- className={index === selectedIndex ? 'selected-item-container' : ''}
- onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etSelectedIndex(index)}</span>
- &gt;
- &lt;FeedItem item={item} /&gt;
- &lt;/div&gt;
- ))}
- {hasMore &amp;&amp; (
- &lt;div id="load-more-sentinel" className="loading-more"&gt;
- {loadingMore ? 'Loading more...' : ''}
- &lt;/div&gt;
- )}
- &lt;/ul&gt;
- )}
- &lt;/div&gt;
- );
+ return (
+ &lt;div className="feed-items"&gt;
+ {items.length === 0 ? (
+<span class="branch-0 cbranch-no" title="branch not covered" > &lt;p&gt;No items found.&lt;/p&gt;</span>
+ ) : (
+ &lt;ul className="item-list"&gt;
+ {items.map((item, index) =&gt; (
+ &lt;div
+ id={`item-${index}`}
+ key={item._id}
+ data-index={index}
+ data-selected={index === selectedIndex}
+ onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etSelectedIndex(index)}</span>
+ &gt;
+ &lt;FeedItem item={item} /&gt;
+ &lt;/div&gt;
+ ))}
+ {hasMore &amp;&amp; (
+ &lt;div id="load-more-sentinel" className="loading-more"&gt;
+ {loadingMore ? 'Loading more...' : ''}
+ &lt;/div&gt;
+ )}
+ &lt;/ul&gt;
+ )}
+ &lt;/div&gt;
+ );
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/FeedList.css.html b/frontend/coverage/src/components/FeedList.css.html
index fe60b9d..2b93e18 100644
--- a/frontend/coverage/src/components/FeedList.css.html
+++ b/frontend/coverage/src/components/FeedList.css.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedList.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 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> FeedList.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>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedList.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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -156,7 +160,159 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<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>
+<a name='L123'></a><a href='#L123'>123</a>
+<a name='L124'></a><a href='#L124'>124</a>
+<a name='L125'></a><a href='#L125'>125</a>
+<a name='L126'></a><a href='#L126'>126</a>
+<a name='L127'></a><a href='#L127'>127</a>
+<a name='L128'></a><a href='#L128'>128</a>
+<a name='L129'></a><a href='#L129'>129</a>
+<a name='L130'></a><a href='#L130'>130</a>
+<a name='L131'></a><a href='#L131'>131</a>
+<a name='L132'></a><a href='#L132'>132</a>
+<a name='L133'></a><a href='#L133'>133</a>
+<a name='L134'></a><a href='#L134'>134</a>
+<a name='L135'></a><a href='#L135'>135</a>
+<a name='L136'></a><a href='#L136'>136</a>
+<a name='L137'></a><a href='#L137'>137</a>
+<a name='L138'></a><a href='#L138'>138</a>
+<a name='L139'></a><a href='#L139'>139</a>
+<a name='L140'></a><a href='#L140'>140</a>
+<a name='L141'></a><a href='#L141'>141</a>
+<a name='L142'></a><a href='#L142'>142</a>
+<a name='L143'></a><a href='#L143'>143</a>
+<a name='L144'></a><a href='#L144'>144</a>
+<a name='L145'></a><a href='#L145'>145</a>
+<a name='L146'></a><a href='#L146'>146</a>
+<a name='L147'></a><a href='#L147'>147</a>
+<a name='L148'></a><a href='#L148'>148</a>
+<a name='L149'></a><a href='#L149'>149</a>
+<a name='L150'></a><a href='#L150'>150</a>
+<a name='L151'></a><a href='#L151'>151</a>
+<a name='L152'></a><a href='#L152'>152</a>
+<a name='L153'></a><a href='#L153'>153</a>
+<a name='L154'></a><a href='#L154'>154</a>
+<a name='L155'></a><a href='#L155'>155</a>
+<a name='L156'></a><a href='#L156'>156</a>
+<a name='L157'></a><a href='#L157'>157</a>
+<a name='L158'></a><a href='#L158'>158</a>
+<a name='L159'></a><a href='#L159'>159</a>
+<a name='L160'></a><a href='#L160'>160</a>
+<a name='L161'></a><a href='#L161'>161</a>
+<a name='L162'></a><a href='#L162'>162</a>
+<a name='L163'></a><a href='#L163'>163</a>
+<a name='L164'></a><a href='#L164'>164</a>
+<a name='L165'></a><a href='#L165'>165</a>
+<a name='L166'></a><a href='#L166'>166</a>
+<a name='L167'></a><a href='#L167'>167</a>
+<a name='L168'></a><a href='#L168'>168</a>
+<a name='L169'></a><a href='#L169'>169</a>
+<a name='L170'></a><a href='#L170'>170</a>
+<a name='L171'></a><a href='#L171'>171</a>
+<a name='L172'></a><a href='#L172'>172</a>
+<a name='L173'></a><a href='#L173'>173</a>
+<a name='L174'></a><a href='#L174'>174</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -254,120 +410,195 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">.feed-list {
- /* Removed card styling */
- padding: 0;
- background: transparent;
+ padding: 1rem;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ color: #777;
+ /* specific v1 color */
+ font-size: 0.8rem;
}
&nbsp;
-.feed-list h2 {
- font-size: 1.2rem;
- margin-bottom: 0.5rem;
- border-bottom: 1px solid #999;
- padding-bottom: 0.25rem;
- text-transform: uppercase;
- letter-spacing: 1px;
+.feed-list h1.logo {
+ font-size: 2rem;
+ /* match v1 */
+ margin: 0 0 1rem 0;
+ line-height: 1;
+ cursor: pointer;
+ position: sticky;
+ top: 0;
+ background: var(--sidebar-bg);
+ z-index: 10;
+ padding-bottom: 0.5rem;
+ color: var(--text-color);
+ /* Usually dark/white depending on theme, v1 was white on blue? No, white on fixed header? No, v1 logo class says color: white. But sidebar is #ccc. */
+ /* In v1 logo was fixed top left (blue header bar?). In v2 sidebar is #ccc.
+ Let's use theme text color but maybe bolder? */
}
&nbsp;
-.feed-list-items,
-.tag-list-items,
-.filter-list {
- list-style: none;
- padding: 0;
- margin: 0;
+/* Override logo color if necessary for themes */
+.theme-light .feed-list h1.logo {
+ color: #333;
}
&nbsp;
-.sidebar-feed-item {
- padding: 0.25rem 0;
- border-bottom: none;
- /* Clean look */
- display: flex;
- justify-content: space-between;
- align-items: center;
+.theme-dark .feed-list h1.logo {
+ color: #eee;
}
&nbsp;
-.feed-title {
- color: #333;
- text-decoration: none;
- font-size: 0.9rem;
+.search-section {
+ margin-bottom: 1rem;
}
&nbsp;
-.feed-title:hover {
- text-decoration: underline;
- color: #000;
+.search-input {
+ width: 100%;
+ padding: 0.25rem;
+ border: 1px solid var(--border-color, #999);
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 0.8rem;
+ font-family: inherit;
+ border-radius: 0;
+ /* v1 didn't have rounded inputs usually */
}
&nbsp;
-.feed-category {
- display: none;
- /* Hide category in sidebar list to save space/match legacy simplicity if needed */
+.section-header {
+ font-size: 1rem;
+ /* v1 h4 size? */
+ font-weight: bold;
+ margin: 1rem 0 0.25rem 0;
+ cursor: pointer;
+ user-select: none;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ color: #333;
+ /* Darker than list items */
+ text-transform: lowercase;
+ font-variant: small-caps;
+}
+&nbsp;
+.filter-list,
+.tag-list-items,
+.feed-list-items,
+.nav-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
}
&nbsp;
-.tag-section {
- margin-top: 2rem;
+.filter-list li,
+.nav-list li {
+ margin-bottom: 0.1rem;
}
&nbsp;
+.filter-list a,
+.nav-list a,
.tag-link,
-.filter-list li a {
- color: #333;
- text-decoration: none;
- font-size: 0.9rem;
- display: block;
- padding: 0.1rem 0;
+.feed-title,
+.logout-link {
+ text-decoration: none;
+ color: var(--link-color, blue);
+ font-size: 0.8rem;
+ /* Matches v1 .75em approx */
+ display: block;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+ font-family: inherit;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
&nbsp;
+.filter-list a:hover,
+.nav-list a:hover,
.tag-link:hover,
-.filter-list li a:hover {
- text-decoration: underline;
- background: transparent;
- /* No hover bg */
- color: #000;
+.feed-title:hover,
+.logout-link:hover {
+ text-decoration: underline;
+ color: var(--link-color, blue);
}
&nbsp;
-.filter-section {
- margin-bottom: 2rem;
+.filter-list a.active,
+.tag-link.active,
+.feed-title.active {
+ font-weight: bold;
+ color: #000;
+ /* Active state black */
}
&nbsp;
-.filter-list {
- display: block;
- /* Stack vertically */
+.tag-item,
+.sidebar-feed-item {
+ margin-bottom: 0;
}
&nbsp;
-.filter-list {
- display: block;
+.feed-category {
+ display: none;
}
&nbsp;
-.filter-list li a {
- text-decoration: none;
- color: #333;
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
- font-weight: bold;
- font-variant: small-caps;
- text-transform: lowercase;
- font-size: 1.1rem;
- display: block;
- margin-bottom: 0.5rem;
+.nav-section {
+ margin-top: 2rem;
+ border-top: 1px solid var(--border-color, #999);
+ padding-top: 0.5rem;
}
&nbsp;
-.filter-list li a:hover {
- color: blue;
- background-color: transparent;
+.logout-link {
+ text-align: left;
+ width: 100%;
+ color: #777;
+ /* Make logout less prominent */
+}
+&nbsp;
+.logout-link:hover {
+ color: var(--link-color, blue);
+}
+&nbsp;
+.theme-section {
+ margin-top: 1rem;
+}
+&nbsp;
+.theme-selector {
+ display: flex;
+ gap: 0.5rem;
+}
+&nbsp;
+.theme-selector button {
+ background: transparent;
+ border: 1px solid var(--border-color, #999);
+ cursor: pointer;
+ padding: 0.1rem 0.3rem;
+ font-size: 0.9rem;
+ border-radius: 0;
+}
+&nbsp;
+.theme-selector button.active {
+ background: var(--border-color, #999);
+ color: white;
+}
+&nbsp;
+/* Scrollbar styling for webkit */
+.dashboard-sidebar::-webkit-scrollbar {
+ width: 4px;
+}
+&nbsp;
+.dashboard-sidebar::-webkit-scrollbar-thumb {
+ background-color: var(--border-color, #999);
}</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/FeedList.tsx.html b/frontend/coverage/src/components/FeedList.tsx.html
index ba7d81f..acb2ede 100644
--- a/frontend/coverage/src/components/FeedList.tsx.html
+++ b/frontend/coverage/src/components/FeedList.tsx.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/FeedList.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 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> FeedList.tsx
- </h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">91.66% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">22/24</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">82.35% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">14/17</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">100% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">8/8</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">100% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">20/20</span>
- </div>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedList.tsx</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">79.54% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>35/44</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">64.86% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>24/37</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">64.7% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>11/17</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">82.05% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>32/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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line high"></div>
- <pre><table class="coverage">
+ </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>
@@ -139,18 +143,153 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">11x</span>
-<span class="cline-any cline-yes">11x</span>
-<span class="cline-any cline-yes">11x</span>
-<span class="cline-any cline-yes">11x</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">11x</span>
+<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>
+<a name='L123'></a><a href='#L123'>123</a>
+<a name='L124'></a><a href='#L124'>124</a>
+<a name='L125'></a><a href='#L125'>125</a>
+<a name='L126'></a><a href='#L126'>126</a>
+<a name='L127'></a><a href='#L127'>127</a>
+<a name='L128'></a><a href='#L128'>128</a>
+<a name='L129'></a><a href='#L129'>129</a>
+<a name='L130'></a><a href='#L130'>130</a>
+<a name='L131'></a><a href='#L131'>131</a>
+<a name='L132'></a><a href='#L132'>132</a>
+<a name='L133'></a><a href='#L133'>133</a>
+<a name='L134'></a><a href='#L134'>134</a>
+<a name='L135'></a><a href='#L135'>135</a>
+<a name='L136'></a><a href='#L136'>136</a>
+<a name='L137'></a><a href='#L137'>137</a>
+<a name='L138'></a><a href='#L138'>138</a>
+<a name='L139'></a><a href='#L139'>139</a>
+<a name='L140'></a><a href='#L140'>140</a>
+<a name='L141'></a><a href='#L141'>141</a>
+<a name='L142'></a><a href='#L142'>142</a>
+<a name='L143'></a><a href='#L143'>143</a>
+<a name='L144'></a><a href='#L144'>144</a>
+<a name='L145'></a><a href='#L145'>145</a>
+<a name='L146'></a><a href='#L146'>146</a>
+<a name='L147'></a><a href='#L147'>147</a>
+<a name='L148'></a><a href='#L148'>148</a>
+<a name='L149'></a><a href='#L149'>149</a>
+<a name='L150'></a><a href='#L150'>150</a>
+<a name='L151'></a><a href='#L151'>151</a>
+<a name='L152'></a><a href='#L152'>152</a>
+<a name='L153'></a><a href='#L153'>153</a>
+<a name='L154'></a><a href='#L154'>154</a>
+<a name='L155'></a><a href='#L155'>155</a>
+<a name='L156'></a><a href='#L156'>156</a>
+<a name='L157'></a><a href='#L157'>157</a>
+<a name='L158'></a><a href='#L158'>158</a>
+<a name='L159'></a><a href='#L159'>159</a>
+<a name='L160'></a><a href='#L160'>160</a>
+<a name='L161'></a><a href='#L161'>161</a>
+<a name='L162'></a><a href='#L162'>162</a>
+<a name='L163'></a><a href='#L163'>163</a>
+<a name='L164'></a><a href='#L164'>164</a>
+<a name='L165'></a><a href='#L165'>165</a>
+<a name='L166'></a><a href='#L166'>166</a>
+<a name='L167'></a><a href='#L167'>167</a>
+<a name='L168'></a><a href='#L168'>168</a>
+<a name='L169'></a><a href='#L169'>169</a>
+<a name='L170'></a><a href='#L170'>170</a>
+<a name='L171'></a><a href='#L171'>171</a>
+<a name='L172'></a><a href='#L172'>172</a>
+<a name='L173'></a><a href='#L173'>173</a>
+<a name='L174'></a><a href='#L174'>174</a>
+<a name='L175'></a><a href='#L175'>175</a>
+<a name='L176'></a><a href='#L176'>176</a>
+<a name='L177'></a><a href='#L177'>177</a>
+<a name='L178'></a><a href='#L178'>178</a>
+<a name='L179'></a><a href='#L179'>179</a>
+<a name='L180'></a><a href='#L180'>180</a>
+<a name='L181'></a><a href='#L181'>181</a>
+<a name='L182'></a><a href='#L182'>182</a>
+<a name='L183'></a><a href='#L183'>183</a>
+<a name='L184'></a><a href='#L184'>184</a>
+<a name='L185'></a><a href='#L185'>185</a>
+<a name='L186'></a><a href='#L186'>186</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
@@ -172,8 +311,56 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">11x</span>
-<span class="cline-any cline-yes">5x</span>
+<span class="cline-any cline-yes">13x</span>
+<span class="cline-any cline-yes">7x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">6x</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">6x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -191,7 +378,20 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -207,7 +407,6 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -216,107 +415,226 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useNavigate, useSearchParams, useLocation, useParams } from 'react-router-dom';
import type { Feed, Category } from '../types';
import './FeedList.css';
+import { apiFetch } from '../utils';
+&nbsp;
+export default function FeedList({
+ theme,
+ setTheme,
+ setSidebarVisible,
+}: {
+ theme: string;
+ setTheme: (t: string) =&gt; void;
+ setSidebarVisible: (visible: boolean) =&gt; void;
+}) {
+ const [feeds, setFeeds] = useState&lt;Feed[]&gt;([]);
+ const [tags, setTags] = useState&lt;Category[]&gt;([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [feedsExpanded, setFeedsExpanded] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const location = useLocation();
+ const { feedId, tagName } = useParams();
+&nbsp;
+ const currentFilter =
+ searchParams.get('filter') ||
+ (location.pathname === '/' &amp;&amp; !feedId &amp;&amp; !tagName ? 'unread' : <span class="branch-1 cbranch-no" title="branch not covered" >'');</span>
+&nbsp;
+ const handleSearch = <span class="fstat-no" title="function not covered" >(e</span>: React.FormEvent) =&gt; {
+<span class="cstat-no" title="statement not covered" > e.preventDefault();</span>
+<span class="cstat-no" title="statement not covered" > if (searchQuery.trim()) {</span>
+<span class="cstat-no" title="statement not covered" > navigate(`/?q=${encodeURIComponent(searchQuery.trim())}`);</span>
+ }
+ };
+&nbsp;
+ const toggleFeeds = () =&gt; {
+ setFeedsExpanded(!feedsExpanded);
+ };
+&nbsp;
+ useEffect(() =&gt; {
+ Promise.all([
+ apiFetch('/api/feed/').then((res) =&gt; {
+ <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();
+ }),
+ apiFetch('/api/tag').then((res) =&gt; {
+ <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 tags');</span>
+ return res.json();
+ }),
+ ])
+ .then(([feedsData, tagsData]) =&gt; {
+ setFeeds(feedsData);
+ setTags(tagsData);
+ setLoading(false);
+ })
+ .catch((err) =&gt; {
+ setError(err.message);
+ setLoading(false);
+ });
+ }, []);
&nbsp;
-export default function FeedList() {
- const [feeds, setFeeds] = useState&lt;Feed[]&gt;([]);
- const [tags, setTags] = useState&lt;Category[]&gt;([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
+ if (loading) return &lt;div className="feed-list-loading"&gt;Loading feeds...&lt;/div&gt;;
+ if (error) return &lt;div className="feed-list-error"&gt;Error: {error}&lt;/div&gt;;
&nbsp;
- useEffect(() =&gt; {
- Promise.all([
- fetch('/api/feed/').then(res =&gt; {
- <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();
- }),
- fetch('/api/tag').then(res =&gt; {
- <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 tags');</span>
- return res.json();
- })
- ])
- .then(([feedsData, tagsData]) =&gt; {
- setFeeds(feedsData);
- setTags(tagsData);
- setLoading(false);
- })
- .catch((err) =&gt; {
- setError(err.message);
- setLoading(false);
- });
- }, []);
+ const handleLogout = () =&gt; {
+ apiFetch('/api/logout', { method: 'POST' }).then(() =&gt; (window.location.href = '/v2/login'));
+ };
&nbsp;
- if (loading) return &lt;div className="feed-list-loading"&gt;Loading feeds...&lt;/div&gt;;
- if (error) return &lt;div className="feed-list-error"&gt;Error: {error}&lt;/div&gt;;
+ return (
+ &lt;div className="feed-list"&gt;
+ &lt;h1 className="logo" onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etSidebarVisible(false)}&gt;</span>
+ 🐱
+ &lt;/h1&gt;
&nbsp;
- return (
- &lt;div className="feed-list"&gt;
- &lt;div className="filter-section"&gt;
- &lt;ul className="filter-list"&gt;
- &lt;li&gt;&lt;Link to="/?filter=unread"&gt;Unread&lt;/Link&gt;&lt;/li&gt;
- &lt;li&gt;&lt;Link to="/?filter=all"&gt;All&lt;/Link&gt;&lt;/li&gt;
- &lt;li&gt;&lt;Link to="/?filter=starred"&gt;Starred&lt;/Link&gt;&lt;/li&gt;
- &lt;/ul&gt;
- &lt;/div&gt;
- &lt;div className="feed-section"&gt;
- &lt;h2&gt;Feeds&lt;/h2&gt;
- {feeds.length === 0 ? (
- &lt;p&gt;No feeds found.&lt;/p&gt;
- ) : (
- &lt;ul className="feed-list-items"&gt;
- {feeds.map((feed) =&gt; (
- &lt;li key={feed._id} className="sidebar-feed-item"&gt;
- &lt;Link to={`/feed/${feed._id}`} className="feed-title"&gt;
- {feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >feed.url}</span>
- &lt;/Link&gt;
- {feed.category &amp;&amp; &lt;span className="feed-category"&gt;{feed.category}&lt;/span&gt;}
- &lt;/li&gt;
- ))}
- &lt;/ul&gt;
- )}
- &lt;/div&gt;
+ &lt;div className="search-section"&gt;
+ &lt;form onSubmit={handleSearch} className="search-form"&gt;
+ &lt;input
+ type="search"
+ placeholder="search..."
+ value={searchQuery}
+ onChange={<span class="fstat-no" title="function not covered" >(e</span>) =&gt; <span class="cstat-no" title="statement not covered" >setSearchQuery(e.target.value)}</span>
+ className="search-input"
+ /&gt;
+ &lt;/form&gt;
+ &lt;/div&gt;
&nbsp;
- {tags &amp;&amp; tags.length &gt; 0 &amp;&amp; (
- &lt;div className="tag-section"&gt;
- &lt;h2&gt;Tags&lt;/h2&gt;
- &lt;ul className="tag-list-items"&gt;
- {tags.map((tag) =&gt; (
- &lt;li key={tag.title} className="tag-item"&gt;
- &lt;Link to={`/tag/${encodeURIComponent(tag.title)}`} className="tag-link"&gt;
- {tag.title}
- &lt;/Link&gt;
- &lt;/li&gt;
- ))}
- &lt;/ul&gt;
- &lt;/div&gt;
- )}
+ &lt;div className="filter-section"&gt;
+ &lt;ul className="filter-list"&gt;
+ &lt;li className="unread_filter"&gt;
+ &lt;Link to="/?filter=unread" className={currentFilter === 'unread' ? 'active' : <span class="branch-1 cbranch-no" title="branch not covered" >''}&gt;</span>
+ unread
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ &lt;li className="all_filter"&gt;
+ &lt;Link to="/?filter=all" className={currentFilter === 'all' ? <span class="branch-0 cbranch-no" title="branch not covered" >'active' : '</span>'}&gt;
+ all
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ &lt;li className="starred_filter"&gt;
+ &lt;Link to="/?filter=starred" className={currentFilter === 'starred' ? <span class="branch-0 cbranch-no" title="branch not covered" >'active' : '</span>'}&gt;
+ starred
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+&nbsp;
+ &lt;div className="tag-section"&gt;
+ &lt;h4 onClick={<span class="fstat-no" title="function not covered" >() =&gt; {</span> }} className="section-header"&gt;
+ Tags
+ &lt;/h4&gt;
+ &lt;ul className="tag-list-items"&gt;
+ {tags.map((tag) =&gt; (
+ &lt;li key={tag.title} className="tag-item"&gt;
+ &lt;Link
+ to={`/tag/${encodeURIComponent(tag.title)}`}
+ className={`tag-link ${tagName === tag.title ? <span class="branch-0 cbranch-no" title="branch not covered" >'active' : '</span>'}`}
+ &gt;
+ {tag.title}
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ ))}
+ &lt;/ul&gt;
+ &lt;/div&gt;
+&nbsp;
+ &lt;div className="feed-section"&gt;
+ &lt;h4 onClick={toggleFeeds} className="section-header"&gt;
+ Feeds
+ &lt;/h4&gt;
+ {feedsExpanded &amp;&amp;
+ (feeds.length === 0 ? (
+ &lt;p&gt;No feeds found.&lt;/p&gt;
+ ) : (
+ &lt;ul className="feed-list-items"&gt;
+ {feeds.map((feed) =&gt; (
+ &lt;li key={feed._id} className="sidebar-feed-item"&gt;
+ &lt;Link
+ to={`/feed/${feed._id}`}
+ className={`feed-title ${feedId === String(feed._id) ? <span class="branch-0 cbranch-no" title="branch not covered" >'active' : '</span>'}`}
+ &gt;
+ {feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >feed.url}</span>
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ ))}
+ &lt;/ul&gt;
+ ))}
+ &lt;/div&gt;
+&nbsp;
+ &lt;div className="nav-section"&gt;
+ &lt;ul className="nav-list"&gt;
+ &lt;li&gt;
+ &lt;Link to="/settings" className="nav-link"&gt;
+ settings
+ &lt;/Link&gt;
+ &lt;/li&gt;
+ &lt;li&gt;
+ &lt;button onClick={handleLogout} className="logout-link"&gt;
+ logout
+ &lt;/button&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+&nbsp;
+ &lt;div className="theme-section"&gt;
+ &lt;div className="theme-selector"&gt;
+ &lt;button
+ onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etTheme('light')}</span>
+ className={theme === 'light' ? 'active' : <span class="branch-1 cbranch-no" title="branch not covered" >''}</span>
+ title="Light Theme"
+ &gt;
+ ☀️
+ &lt;/button&gt;
+ &lt;button
+ onClick={<span class="fstat-no" title="function not covered" >() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etTheme('dark')}</span>
+ className={theme === 'dark' ? <span class="branch-0 cbranch-no" title="branch not covered" >'active' : '</span>'}
+ title="Dark Theme"
+ &gt;
+ 🌙
+ &lt;/button&gt;
&lt;/div&gt;
- );
+ &lt;/div&gt;
+ &lt;/div&gt;
+ );
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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/Login.css.html b/frontend/coverage/src/components/Login.css.html
index bb3654e..031618b 100644
--- a/frontend/coverage/src/components/Login.css.html
+++ b/frontend/coverage/src/components/Login.css.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/Login.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 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> Login.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>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> Login.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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -233,7 +237,7 @@
text-align: center;
}
&nbsp;
-button[type="submit"] {
+button[type='submit'] {
width: 100%;
padding: 0.75rem;
background-color: #007bff;
@@ -245,27 +249,26 @@ button[type="submit"] {
transition: background-color 0.2s;
}
&nbsp;
-button[type="submit"]:hover {
+button[type='submit']:hover {
background-color: #0056b3;
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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/Login.tsx.html b/frontend/coverage/src/components/Login.tsx.html
index f29e3cb..43fb613 100644
--- a/frontend/coverage/src/components/Login.tsx.html
+++ b/frontend/coverage/src/components/Login.tsx.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components/Login.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 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> Login.tsx
- </h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">100% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">17/17</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">83.33% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">5/6</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">100% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">3/3</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">100% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">17/17</span>
- </div>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> Login.tsx</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">100% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>17/17</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">83.33% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>5/6</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">100% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>3/3</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">100% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>17/17</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line high"></div>
- <pre><table class="coverage">
+ </div>
+ <div class='status-line high'></div>
+ <pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
@@ -113,7 +117,11 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -171,74 +179,75 @@
import { useNavigate } from 'react-router-dom';
import './Login.css';
&nbsp;
+import { apiFetch } from '../utils';
+&nbsp;
export default function Login() {
- const [password, setPassword] = useState('');
- const [error, setError] = useState('');
- const navigate = useNavigate();
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
&nbsp;
- const handleSubmit = async (e: FormEvent) =&gt; {
- e.preventDefault();
- setError('');
+ const handleSubmit = async (e: FormEvent) =&gt; {
+ e.preventDefault();
+ setError('');
&nbsp;
- try {
- // Use URLSearchParams to send as form-urlencoded, matching backend expectation
- const params = new URLSearchParams();
- params.append('password', password);
+ try {
+ // Use URLSearchParams to send as form-urlencoded, matching backend expectation
+ const params = new URLSearchParams();
+ params.append('password', password);
&nbsp;
- const res = await fetch('/api/login', {
- method: 'POST',
- body: params,
- });
+ const res = await apiFetch('/api/login', {
+ method: 'POST',
+ body: params,
+ });
&nbsp;
- if (res.ok) {
- navigate('/');
- } else {
- const data = await res.json();
- setError(data.message || <span class="branch-1 cbranch-no" title="branch not covered" >'Login failed')</span>;
- }
- } catch (err) {
- setError('Network error');
- }
- };
+ if (res.ok) {
+ navigate('/');
+ } else {
+ const data = await res.json();
+ setError(data.message || <span class="branch-1 cbranch-no" title="branch not covered" >'Login failed')</span>;
+ }
+ } catch (err) {
+ setError('Network error');
+ }
+ };
&nbsp;
- return (
- &lt;div className="login-container"&gt;
- &lt;form onSubmit={handleSubmit} className="login-form"&gt;
- &lt;h1&gt;neko rss mode&lt;/h1&gt;
- &lt;div className="form-group"&gt;
- &lt;label htmlFor="password"&gt;password&lt;/label&gt;
- &lt;input
- id="password"
- type="password"
- value={password}
- onChange={(e) =&gt; setPassword(e.target.value)}
- autoFocus
- /&gt;
- &lt;/div&gt;
- {error &amp;&amp; &lt;div className="error-message"&gt;{error}&lt;/div&gt;}
- &lt;button type="submit"&gt;login&lt;/button&gt;
- &lt;/form&gt;
+ return (
+ &lt;div className="login-container"&gt;
+ &lt;form onSubmit={handleSubmit} className="login-form"&gt;
+ &lt;h1&gt;neko rss mode&lt;/h1&gt;
+ &lt;div className="form-group"&gt;
+ &lt;label htmlFor="password"&gt;password&lt;/label&gt;
+ &lt;input
+ id="password"
+ type="password"
+ value={password}
+ onChange={(e) =&gt; setPassword(e.target.value)}
+ autoFocus
+ /&gt;
&lt;/div&gt;
- );
+ {error &amp;&amp; &lt;div className="error-message"&gt;{error}&lt;/div&gt;}
+ &lt;button type="submit"&gt;login&lt;/button&gt;
+ &lt;/form&gt;
+ &lt;/div&gt;
+ );
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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.css.html b/frontend/coverage/src/components/Settings.css.html
index 6a1155e..28a1915 100644
--- a/frontend/coverage/src/components/Settings.css.html
+++ b/frontend/coverage/src/components/Settings.css.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<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 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>
+</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line low"></div>
- <pre><table class="coverage">
+ </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>
@@ -141,7 +145,163 @@
<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">&nbsp;</span>
+<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>
+<a name='L123'></a><a href='#L123'>123</a>
+<a name='L124'></a><a href='#L124'>124</a>
+<a name='L125'></a><a href='#L125'>125</a>
+<a name='L126'></a><a href='#L126'>126</a>
+<a name='L127'></a><a href='#L127'>127</a>
+<a name='L128'></a><a href='#L128'>128</a>
+<a name='L129'></a><a href='#L129'>129</a>
+<a name='L130'></a><a href='#L130'>130</a>
+<a name='L131'></a><a href='#L131'>131</a>
+<a name='L132'></a><a href='#L132'>132</a>
+<a name='L133'></a><a href='#L133'>133</a>
+<a name='L134'></a><a href='#L134'>134</a>
+<a name='L135'></a><a href='#L135'>135</a>
+<a name='L136'></a><a href='#L136'>136</a>
+<a name='L137'></a><a href='#L137'>137</a>
+<a name='L138'></a><a href='#L138'>138</a>
+<a name='L139'></a><a href='#L139'>139</a>
+<a name='L140'></a><a href='#L140'>140</a>
+<a name='L141'></a><a href='#L141'>141</a>
+<a name='L142'></a><a href='#L142'>142</a>
+<a name='L143'></a><a href='#L143'>143</a>
+<a name='L144'></a><a href='#L144'>144</a>
+<a name='L145'></a><a href='#L145'>145</a>
+<a name='L146'></a><a href='#L146'>146</a>
+<a name='L147'></a><a href='#L147'>147</a>
+<a name='L148'></a><a href='#L148'>148</a>
+<a name='L149'></a><a href='#L149'>149</a>
+<a name='L150'></a><a href='#L150'>150</a>
+<a name='L151'></a><a href='#L151'>151</a>
+<a name='L152'></a><a href='#L152'>152</a>
+<a name='L153'></a><a href='#L153'>153</a>
+<a name='L154'></a><a href='#L154'>154</a>
+<a name='L155'></a><a href='#L155'>155</a>
+<a name='L156'></a><a href='#L156'>156</a>
+<a name='L157'></a><a href='#L157'>157</a>
+<a name='L158'></a><a href='#L158'>158</a>
+<a name='L159'></a><a href='#L159'>159</a>
+<a name='L160'></a><a href='#L160'>160</a>
+<a name='L161'></a><a href='#L161'>161</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -224,105 +384,182 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">.settings-page {
- padding: 2rem;
- max-width: 800px;
- margin: 0 auto;
+ padding: 2rem;
+ max-width: 800px;
+ margin: 0 auto;
}
&nbsp;
.add-feed-section {
- background: #f9f9f9;
- padding: 1.5rem;
- border-radius: 8px;
- margin-bottom: 2rem;
- border: 1px solid #eee;
+ background: var(--sidebar-bg);
+ padding: 1.5rem;
+ border-radius: 8px;
+ margin-bottom: 2rem;
+ border: 1px solid var(--border-color);
}
&nbsp;
.add-feed-form {
- display: flex;
- gap: 1rem;
+ display: flex;
+ gap: 1rem;
}
&nbsp;
.feed-input {
- flex: 1;
- padding: 0.5rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 1rem;
+ flex: 1;
+ padding: 0.5rem;
+ border: 1px solid var(--border-color);
+ background: var(--bg-color);
+ color: var(--text-color);
+ border-radius: 4px;
+ font-size: 1rem;
}
&nbsp;
.error-message {
- color: #d32f2f;
- margin-top: 1rem;
+ color: #d32f2f;
+ margin-top: 1rem;
}
&nbsp;
.settings-feed-list {
- list-style: none;
- padding: 0;
- border: 1px solid #eee;
- border-radius: 8px;
+ list-style: none;
+ padding: 0;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
}
&nbsp;
.settings-feed-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem;
- border-bottom: 1px solid #eee;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
}
&nbsp;
.settings-feed-item:last-child {
- border-bottom: none;
+ border-bottom: none;
}
&nbsp;
.feed-info {
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
}
&nbsp;
.feed-title {
- font-weight: bold;
- font-size: 1.1rem;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ font-size: 1.1rem;
}
&nbsp;
.feed-url {
- color: #666;
- font-size: 0.9rem;
+ color: var(--text-color);
+ opacity: 0.6;
+ font-size: 0.9rem;
}
&nbsp;
.delete-btn {
- background: #ff5252;
- color: white;
- border: none;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
+ background: #ff5252;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
}
&nbsp;
.delete-btn:hover {
- background: #ff1744;
+ background: #ff1744;
}
&nbsp;
.delete-btn:disabled {
- background: #ffcdd2;
- cursor: not-allowed;
+ background: #ffcdd2;
+ cursor: not-allowed;
+}
+&nbsp;
+.import-export-section {
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 2rem;
+}
+&nbsp;
+@media (max-width: 600px) {
+ .import-export-section {
+ flex-direction: column;
+ }
+}
+&nbsp;
+.import-section,
+.export-section {
+ flex: 1;
+ background: var(--sidebar-bg);
+ padding: 1.5rem;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+}
+&nbsp;
+.import-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+&nbsp;
+.file-input {
+ font-size: 0.9rem;
+ max-width: 100%;
+}
+&nbsp;
+.export-buttons {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+&nbsp;
+.export-btn {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ background: var(--bg-color);
+ color: var(--link-color);
+ text-decoration: none;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-weight: bold;
+ text-align: center;
+ min-width: 70px;
+}
+&nbsp;
+.export-btn:hover {
+ background: var(--sidebar-bg);
+}
+&nbsp;
+button {
+ cursor: pointer;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-weight: bold;
+}
+&nbsp;
+button:hover:not(:disabled) {
+ background: var(--sidebar-bg);
+}
+&nbsp;
+button:disabled {
+ opacity: 0.5;
+ 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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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
index df6d027..3d8d219 100644
--- a/frontend/coverage/src/components/Settings.tsx.html
+++ b/frontend/coverage/src/components/Settings.tsx.html
@@ -1,64 +1,68 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<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 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>
+</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">56.25% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>36/64</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">41.66% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>10/24</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">63.63% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>14/22</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">62.5% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>35/56</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line medium"></div>
- <pre><table class="coverage">
+ </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>
@@ -180,7 +184,67 @@
<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">&nbsp;</span>
+<a name='L122'></a><a href='#L122'>122</a>
+<a name='L123'></a><a href='#L123'>123</a>
+<a name='L124'></a><a href='#L124'>124</a>
+<a name='L125'></a><a href='#L125'>125</a>
+<a name='L126'></a><a href='#L126'>126</a>
+<a name='L127'></a><a href='#L127'>127</a>
+<a name='L128'></a><a href='#L128'>128</a>
+<a name='L129'></a><a href='#L129'>129</a>
+<a name='L130'></a><a href='#L130'>130</a>
+<a name='L131'></a><a href='#L131'>131</a>
+<a name='L132'></a><a href='#L132'>132</a>
+<a name='L133'></a><a href='#L133'>133</a>
+<a name='L134'></a><a href='#L134'>134</a>
+<a name='L135'></a><a href='#L135'>135</a>
+<a name='L136'></a><a href='#L136'>136</a>
+<a name='L137'></a><a href='#L137'>137</a>
+<a name='L138'></a><a href='#L138'>138</a>
+<a name='L139'></a><a href='#L139'>139</a>
+<a name='L140'></a><a href='#L140'>140</a>
+<a name='L141'></a><a href='#L141'>141</a>
+<a name='L142'></a><a href='#L142'>142</a>
+<a name='L143'></a><a href='#L143'>143</a>
+<a name='L144'></a><a href='#L144'>144</a>
+<a name='L145'></a><a href='#L145'>145</a>
+<a name='L146'></a><a href='#L146'>146</a>
+<a name='L147'></a><a href='#L147'>147</a>
+<a name='L148'></a><a href='#L148'>148</a>
+<a name='L149'></a><a href='#L149'>149</a>
+<a name='L150'></a><a href='#L150'>150</a>
+<a name='L151'></a><a href='#L151'>151</a>
+<a name='L152'></a><a href='#L152'>152</a>
+<a name='L153'></a><a href='#L153'>153</a>
+<a name='L154'></a><a href='#L154'>154</a>
+<a name='L155'></a><a href='#L155'>155</a>
+<a name='L156'></a><a href='#L156'>156</a>
+<a name='L157'></a><a href='#L157'>157</a>
+<a name='L158'></a><a href='#L158'>158</a>
+<a name='L159'></a><a href='#L159'>159</a>
+<a name='L160'></a><a href='#L160'>160</a>
+<a name='L161'></a><a href='#L161'>161</a>
+<a name='L162'></a><a href='#L162'>162</a>
+<a name='L163'></a><a href='#L163'>163</a>
+<a name='L164'></a><a href='#L164'>164</a>
+<a name='L165'></a><a href='#L165'>165</a>
+<a name='L166'></a><a href='#L166'>166</a>
+<a name='L167'></a><a href='#L167'>167</a>
+<a name='L168'></a><a href='#L168'>168</a>
+<a name='L169'></a><a href='#L169'>169</a>
+<a name='L170'></a><a href='#L170'>170</a>
+<a name='L171'></a><a href='#L171'>171</a>
+<a name='L172'></a><a href='#L172'>172</a>
+<a name='L173'></a><a href='#L173'>173</a>
+<a name='L174'></a><a href='#L174'>174</a>
+<a name='L175'></a><a href='#L175'>175</a>
+<a name='L176'></a><a href='#L176'>176</a>
+<a name='L177'></a><a href='#L177'>177</a>
+<a name='L178'></a><a href='#L178'>178</a>
+<a name='L179'></a><a href='#L179'>179</a>
+<a name='L180'></a><a href='#L180'>180</a>
+<a name='L181'></a><a href='#L181'>181</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -191,8 +255,6 @@
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
-<span class="cline-any cline-yes">3x</span>
-<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">4x</span>
@@ -212,6 +274,10 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</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">&nbsp;</span>
@@ -254,6 +320,34 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -281,6 +375,34 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -304,142 +426,200 @@
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import React, { useEffect, useState } from 'react';
import type { Feed } from '../types';
import './Settings.css';
+import { apiFetch } from '../utils';
&nbsp;
export default function Settings() {
- const [feeds, setFeeds] = useState&lt;Feed[]&gt;([]);
- const [newFeedUrl, setNewFeedUrl] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState&lt;string | null&gt;(null);
+ const [feeds, setFeeds] = useState&lt;Feed[]&gt;([]);
+ const [newFeedUrl, setNewFeedUrl] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState&lt;string | null&gt;(null);
&nbsp;
- useEffect(() =&gt; {
+ const [importFile, setImportFile] = useState&lt;File | null&gt;(null);
+&nbsp;
+ const fetchFeeds = () =&gt; {
+ setLoading(true);
+ apiFetch('/api/feed/')
+ .then((res) =&gt; {
+ <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) =&gt; {
+ setFeeds(data);
+ setLoading(false);
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
+&nbsp;
+ useEffect(() =&gt; {
+ fetchFeeds();
+ }, []);
+&nbsp;
+ const handleAddFeed = (e: React.FormEvent) =&gt; {
+ 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>
+&nbsp;
+ setLoading(true);
+ apiFetch('/api/feed/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: newFeedUrl }),
+ })
+ .then((res) =&gt; {
+ <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(() =&gt; {
+ setNewFeedUrl('');
fetchFeeds();
- }, []);
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
+&nbsp;
+ const handleDeleteFeed = (id: number) =&gt; {
+ <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>
&nbsp;
- const fetchFeeds = () =&gt; {
- setLoading(true);
- fetch('/api/feed/')
- .then((res) =&gt; {
- <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) =&gt; {
- setFeeds(data);
- setLoading(false);
- })
- .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
-<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
-<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
- });
- };
+ setLoading(true);
+ apiFetch(`/api/feed/${id}`, {
+ method: 'DELETE',
+ })
+ .then((res) =&gt; {
+ <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) =&gt; f._id !== id));
+ setLoading(false);
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
&nbsp;
- const handleAddFeed = (e: React.FormEvent) =&gt; {
- 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>
+ const handleImport = <span class="fstat-no" title="function not covered" >(e</span>: React.FormEvent) =&gt; {
+<span class="cstat-no" title="statement not covered" > e.preventDefault();</span>
+<span class="cstat-no" title="statement not covered" > if (!importFile) <span class="cstat-no" title="statement not covered" >return;</span></span>
&nbsp;
- setLoading(true);
- fetch('/api/feed/', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ url: newFeedUrl }),
- })
- .then((res) =&gt; {
- <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(() =&gt; {
- 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) =&gt; {
-<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
-<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
- });
- };
+<span class="cstat-no" title="statement not covered" > setLoading(true);</span>
+ const formData = <span class="cstat-no" title="statement not covered" >new FormData();</span>
+<span class="cstat-no" title="statement not covered" > formData.append('file', importFile);</span>
+<span class="cstat-no" title="statement not covered" > formData.append('format', 'opml');</span>
&nbsp;
- const handleDeleteFeed = (id: number) =&gt; {
- <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>
+<span class="cstat-no" title="statement not covered" > apiFetch('/api/import', {</span>
+ method: 'POST',
+ body: formData,
+ })
+ .then(<span class="fstat-no" title="function not covered" >(r</span>es) =&gt; {
+<span class="cstat-no" title="statement not covered" > if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to import feeds');</span></span>
+<span class="cstat-no" title="statement not covered" > return res.json();</span>
+ })
+ .then(<span class="fstat-no" title="function not covered" >() =&gt; {</span>
+<span class="cstat-no" title="statement not covered" > setImportFile(null);</span>
+<span class="cstat-no" title="statement not covered" > fetchFeeds();</span>
+<span class="cstat-no" title="statement not covered" > alert('Import successful!');</span>
+ })
+ .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
+<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
+<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
+ });
+ };
&nbsp;
- setLoading(true);
- fetch(`/api/feed/${id}`, {
- method: 'DELETE',
- })
- .then((res) =&gt; {
- <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) =&gt; f._id !== id));
- setLoading(false);
- })
- .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) =&gt; {
-<span class="cstat-no" title="statement not covered" > setError(err.message);</span>
-<span class="cstat-no" title="statement not covered" > setLoading(false);</span>
- });
- };
+ return (
+ &lt;div className="settings-page"&gt;
+ &lt;h2&gt;Settings&lt;/h2&gt;
&nbsp;
- return (
- &lt;div className="settings-page"&gt;
- &lt;h2&gt;Settings&lt;/h2&gt;
+ &lt;div className="add-feed-section"&gt;
+ &lt;h3&gt;Add New Feed&lt;/h3&gt;
+ &lt;form onSubmit={handleAddFeed} className="add-feed-form"&gt;
+ &lt;input
+ type="url"
+ value={newFeedUrl}
+ onChange={(e) =&gt; setNewFeedUrl(e.target.value)}
+ placeholder="https://example.com/feed.xml"
+ required
+ className="feed-input"
+ disabled={loading}
+ /&gt;
+ &lt;button type="submit" disabled={loading}&gt;
+ Add Feed
+ &lt;/button&gt;
+ &lt;/form&gt;
+ &lt;/div&gt;
&nbsp;
- &lt;div className="add-feed-section"&gt;
- &lt;h3&gt;Add New Feed&lt;/h3&gt;
- &lt;form onSubmit={handleAddFeed} className="add-feed-form"&gt;
- &lt;input
- type="url"
- value={newFeedUrl}
- onChange={(e) =&gt; setNewFeedUrl(e.target.value)}
- placeholder="https://example.com/feed.xml"
- required
- className="feed-input"
- disabled={loading}
- /&gt;
- &lt;button type="submit" disabled={loading}&gt;
- Add Feed
- &lt;/button&gt;
- &lt;/form&gt;
- {error &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >&lt;p className="error-message"&gt;{error}&lt;/p&gt;}</span>
- &lt;/div&gt;
+ &lt;div className="import-export-section"&gt;
+ &lt;div className="import-section"&gt;
+ &lt;h3&gt;Import Feeds (OPML)&lt;/h3&gt;
+ &lt;form onSubmit={handleImport} className="import-form"&gt;
+ &lt;input
+ type="file"
+ accept=".opml,.xml,.txt"
+ onChange={<span class="fstat-no" title="function not covered" >(e</span>) =&gt; <span class="cstat-no" title="statement not covered" >setImportFile(e.target.files?.[0] || null)}</span>
+ className="file-input"
+ disabled={loading}
+ /&gt;
+ &lt;button type="submit" disabled={!importFile || <span class="branch-1 cbranch-no" title="branch not covered" >loading}&gt;</span>
+ Import
+ &lt;/button&gt;
+ &lt;/form&gt;
+ &lt;/div&gt;
&nbsp;
- &lt;div className="feed-list-section"&gt;
- &lt;h3&gt;Manage Feeds&lt;/h3&gt;
- {loading &amp;&amp; &lt;p&gt;Loading...&lt;/p&gt;}
- &lt;ul className="settings-feed-list"&gt;
- {feeds.map((feed) =&gt; (
- &lt;li key={feed._id} className="settings-feed-item"&gt;
- &lt;div className="feed-info"&gt;
- &lt;span className="feed-title"&gt;{feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}&lt;</span>/span&gt;
- &lt;span className="feed-url"&gt;{feed.url}&lt;/span&gt;
- &lt;/div&gt;
- &lt;button
- onClick={() =&gt; handleDeleteFeed(feed._id)}
- className="delete-btn"
- disabled={loading}
- title="Delete Feed"
- &gt;
- Delete
- &lt;/button&gt;
- &lt;/li&gt;
- ))}
- &lt;/ul&gt;
- &lt;/div&gt;
+ &lt;div className="export-section"&gt;
+ &lt;h3&gt;Export Feeds&lt;/h3&gt;
+ &lt;div className="export-buttons"&gt;
+ &lt;a href="/api/export/opml" className="export-btn"&gt;OPML&lt;/a&gt;
+ &lt;a href="/api/export/text" className="export-btn"&gt;Text&lt;/a&gt;
+ &lt;a href="/api/export/json" className="export-btn"&gt;JSON&lt;/a&gt;
+ &lt;/div&gt;
&lt;/div&gt;
- );
+ &lt;/div&gt;
+&nbsp;
+ {error &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >&lt;p className="error-message"&gt;{error}&lt;/p&gt;}</span>
+&nbsp;
+ &lt;div className="feed-list-section"&gt;
+ &lt;h3&gt;Manage Feeds&lt;/h3&gt;
+ {loading &amp;&amp; &lt;p&gt;Loading...&lt;/p&gt;}
+ &lt;ul className="settings-feed-list"&gt;
+ {feeds.map((feed) =&gt; (
+ &lt;li key={feed._id} className="settings-feed-item"&gt;
+ &lt;div className="feed-info"&gt;
+ &lt;span className="feed-title"&gt;{feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}&lt;</span>/span&gt;
+ &lt;span className="feed-url"&gt;{feed.url}&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;button
+ onClick={() =&gt; handleDeleteFeed(feed._id)}
+ className="delete-btn"
+ disabled={loading}
+ title="Delete Feed"
+ &gt;
+ Delete
+ &lt;/button&gt;
+ &lt;/li&gt;
+ ))}
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ );
}
&nbsp;</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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+ <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-14T18:02:09.004Z
+ </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 7e1a0b7..66ca900 100644
--- a/frontend/coverage/src/components/index.html
+++ b/frontend/coverage/src/components/index.html
@@ -1,303 +1,251 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src/components</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 type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
<h1><a href="../../index.html">All files</a> src/components</h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">86.78% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">197/227</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">77.2% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">105/136</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">87.5% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">56/64</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">89.26% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">183/205</span>
- </div>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">79.57% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>226/284</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">70.17% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>120/171</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">75.9% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>63/83</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">81.78% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>211/258</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line high"></div>
- <div class="pad1">
- <table class="coverage-summary">
- <thead>
- <tr>
- <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
- <th
- data-col="pic"
- data-type="number"
- data-fmt="html"
- data-html="true"
- class="pic"
- ></th>
- <th data-col="statements" data-type="number" data-fmt="pct" class="pct">
- Statements
- </th>
- <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
- <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
- <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
- <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td class="file empty" data-value="FeedItem.css">
- <a href="FeedItem.css.html">FeedItem.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>
+ </div>
+ <div class='status-line medium'></div>
+ <div class="pad1">
+<table class="coverage-summary">
+<thead>
+<tr>
+ <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
+ <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
+ <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
+ <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
+ <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
+ <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
+ <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
+</tr>
+</thead>
+<tbody><tr>
+ <td class="file empty" data-value="FeedItem.css"><a href="FeedItem.css.html">FeedItem.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="FeedItem.tsx">
- <a href="FeedItem.tsx.html">FeedItem.tsx</a>
- </td>
- <td data-value="78.94" class="pic medium">
- <div class="chart">
- <div class="cover-fill" style="width: 78%"></div>
- <div class="cover-empty" style="width: 22%"></div>
- </div>
- </td>
- <td data-value="78.94" class="pct medium">78.94%</td>
- <td data-value="19" class="abs medium">15/19</td>
- <td data-value="88.88" class="pct high">88.88%</td>
- <td data-value="18" class="abs high">16/18</td>
- <td data-value="85.71" class="pct high">85.71%</td>
- <td data-value="7" class="abs high">6/7</td>
- <td data-value="78.94" class="pct medium">78.94%</td>
- <td data-value="19" class="abs medium">15/19</td>
- </tr>
+<tr>
+ <td class="file medium" data-value="FeedItem.tsx"><a href="FeedItem.tsx.html">FeedItem.tsx</a></td>
+ <td data-value="78.12" class="pic medium">
+ <div class="chart"><div class="cover-fill" style="width: 78%"></div><div class="cover-empty" style="width: 22%"></div></div>
+ </td>
+ <td data-value="78.12" class="pct medium">78.12%</td>
+ <td data-value="32" class="abs medium">25/32</td>
+ <td data-value="86.95" class="pct high">86.95%</td>
+ <td data-value="23" class="abs high">20/23</td>
+ <td data-value="83.33" class="pct high">83.33%</td>
+ <td data-value="12" class="abs high">10/12</td>
+ <td data-value="80.64" class="pct high">80.64%</td>
+ <td data-value="31" class="abs high">25/31</td>
+ </tr>
- <tr>
- <td class="file empty" data-value="FeedItems.css">
- <a href="FeedItems.css.html">FeedItems.css</a>
- </td>
- <td data-value="0" class="pic empty">
- <div class="chart">
- <div class="cover-fill" style="width: 0%"></div>
- <div class="cover-empty" style="width: 100%"></div>
- </div>
- </td>
- <td data-value="0" class="pct empty">0%</td>
- <td data-value="0" class="abs empty">0/0</td>
- <td data-value="0" class="pct empty">0%</td>
- <td data-value="0" class="abs empty">0/0</td>
- <td data-value="0" class="pct empty">0%</td>
- <td data-value="0" class="abs empty">0/0</td>
- <td data-value="0" class="pct empty">0%</td>
- <td data-value="0" class="abs empty">0/0</td>
- </tr>
+<tr>
+ <td class="file empty" data-value="FeedItems.css"><a href="FeedItems.css.html">FeedItems.css</a></td>
+ <td data-value="0" class="pic empty">
+ <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
+ </td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ </tr>
- <tr>
- <td class="file high" data-value="FeedItems.tsx">
- <a href="FeedItems.tsx.html">FeedItems.tsx</a>
- </td>
- <td data-value="89.34" class="pic high">
- <div class="chart">
- <div class="cover-fill" style="width: 89%"></div>
- <div class="cover-empty" style="width: 11%"></div>
- </div>
- </td>
- <td data-value="89.34" class="pct high">89.34%</td>
- <td data-value="122" class="abs high">109/122</td>
- <td data-value="77.21" class="pct medium">77.21%</td>
- <td data-value="79" class="abs medium">61/79</td>
- <td data-value="86.2" class="pct high">86.2%</td>
- <td data-value="29" class="abs high">25/29</td>
- <td data-value="89.09" class="pct high">89.09%</td>
- <td data-value="110" class="abs high">98/110</td>
- </tr>
+<tr>
+ <td class="file high" data-value="FeedItems.tsx"><a href="FeedItems.tsx.html">FeedItems.tsx</a></td>
+ <td data-value="88.97" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 88%"></div><div class="cover-empty" style="width: 12%"></div></div>
+ </td>
+ <td data-value="88.97" class="pct high">88.97%</td>
+ <td data-value="127" class="abs high">113/127</td>
+ <td data-value="75.3" class="pct medium">75.3%</td>
+ <td data-value="81" class="abs medium">61/81</td>
+ <td data-value="86.2" class="pct high">86.2%</td>
+ <td data-value="29" class="abs high">25/29</td>
+ <td data-value="88.69" class="pct high">88.69%</td>
+ <td data-value="115" class="abs high">102/115</td>
+ </tr>
- <tr>
- <td class="file empty" data-value="FeedList.css">
- <a href="FeedList.css.html">FeedList.css</a>
- </td>
- <td data-value="0" class="pic empty">
- <div class="chart">
- <div class="cover-fill" style="width: 0%"></div>
- <div class="cover-empty" style="width: 100%"></div>
- </div>
- </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 empty" data-value="FeedList.css"><a href="FeedList.css.html">FeedList.css</a></td>
+ <td data-value="0" class="pic empty">
+ <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
+ </td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ </tr>
- <tr>
- <td class="file high" data-value="FeedList.tsx">
- <a href="FeedList.tsx.html">FeedList.tsx</a>
- </td>
- <td data-value="91.66" class="pic high">
- <div class="chart">
- <div class="cover-fill" style="width: 91%"></div>
- <div class="cover-empty" style="width: 9%"></div>
- </div>
- </td>
- <td data-value="91.66" class="pct high">91.66%</td>
- <td data-value="24" class="abs high">22/24</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="100" class="pct high">100%</td>
- <td data-value="8" class="abs high">8/8</td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="20" class="abs high">20/20</td>
- </tr>
+<tr>
+ <td class="file medium" data-value="FeedList.tsx"><a href="FeedList.tsx.html">FeedList.tsx</a></td>
+ <td data-value="79.54" class="pic medium">
+ <div class="chart"><div class="cover-fill" style="width: 79%"></div><div class="cover-empty" style="width: 21%"></div></div>
+ </td>
+ <td data-value="79.54" class="pct medium">79.54%</td>
+ <td data-value="44" class="abs medium">35/44</td>
+ <td data-value="64.86" class="pct medium">64.86%</td>
+ <td data-value="37" class="abs medium">24/37</td>
+ <td data-value="64.7" class="pct medium">64.7%</td>
+ <td data-value="17" class="abs medium">11/17</td>
+ <td data-value="82.05" class="pct high">82.05%</td>
+ <td data-value="39" class="abs high">32/39</td>
+ </tr>
- <tr>
- <td class="file empty" data-value="Login.css">
- <a href="Login.css.html">Login.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 empty" data-value="Login.css"><a href="Login.css.html">Login.css</a></td>
+ <td data-value="0" class="pic empty">
+ <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
+ </td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ </tr>
- <tr>
- <td class="file high" data-value="Login.tsx">
- <a href="Login.tsx.html">Login.tsx</a>
- </td>
- <td data-value="100" class="pic high">
- <div class="chart">
- <div class="cover-fill cover-full" style="width: 100%"></div>
- <div class="cover-empty" style="width: 0%"></div>
- </div>
- </td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="17" class="abs high">17/17</td>
- <td data-value="83.33" class="pct high">83.33%</td>
- <td data-value="6" class="abs high">5/6</td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="3" class="abs high">3/3</td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="17" class="abs high">17/17</td>
- </tr>
+<tr>
+ <td class="file high" data-value="Login.tsx"><a href="Login.tsx.html">Login.tsx</a></td>
+ <td data-value="100" class="pic high">
+ <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
+ </td>
+ <td data-value="100" class="pct high">100%</td>
+ <td data-value="17" class="abs high">17/17</td>
+ <td data-value="83.33" class="pct high">83.33%</td>
+ <td data-value="6" class="abs high">5/6</td>
+ <td data-value="100" class="pct high">100%</td>
+ <td data-value="3" class="abs high">3/3</td>
+ <td data-value="100" class="pct high">100%</td>
+ <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 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>
- <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-13T21:49:58.924Z
- </div>
- <script src="../../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../../sorter.js"></script>
- <script src="../../block-navigation.js"></script>
- </body>
+<tr>
+ <td class="file medium" data-value="Settings.tsx"><a href="Settings.tsx.html">Settings.tsx</a></td>
+ <td data-value="56.25" class="pic medium">
+ <div class="chart"><div class="cover-fill" style="width: 56%"></div><div class="cover-empty" style="width: 44%"></div></div>
+ </td>
+ <td data-value="56.25" class="pct medium">56.25%</td>
+ <td data-value="64" class="abs medium">36/64</td>
+ <td data-value="41.66" class="pct low">41.66%</td>
+ <td data-value="24" class="abs low">10/24</td>
+ <td data-value="63.63" class="pct medium">63.63%</td>
+ <td data-value="22" class="abs medium">14/22</td>
+ <td data-value="62.5" class="pct medium">62.5%</td>
+ <td data-value="56" class="abs medium">35/56</td>
+ </tr>
+
+</tbody>
+</table>
+</div>
+ <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-14T18:02:09.004Z
+ </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/index.html b/frontend/coverage/src/index.html
index 14e9f51..19a8dfd 100644
--- a/frontend/coverage/src/index.html
+++ b/frontend/coverage/src/index.html
@@ -1,139 +1,146 @@
+
<!doctype html>
<html lang="en">
- <head>
+
+<head>
<title>Code coverage report for src</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 type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../sort-arrow-sprite.png);
+ }
</style>
- </head>
-
- <body>
- <div class="wrapper">
- <div class="pad1">
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
<h1><a href="../index.html">All files</a> src</h1>
- <div class="clearfix">
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Statements</span>
- <span class="fraction">15/19</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">66.66% </span>
- <span class="quiet">Branches</span>
- <span class="fraction">4/6</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">77.77% </span>
- <span class="quiet">Functions</span>
- <span class="fraction">7/9</span>
- </div>
-
- <div class="fl pad1y space-right2">
- <span class="strong">78.94% </span>
- <span class="quiet">Lines</span>
- <span class="fraction">15/19</span>
- </div>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">76.47% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>26/34</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">69.23% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>18/26</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">70% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>7/10</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">78.78% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>26/33</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.
+ 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>
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
</template>
- </div>
- <div class="status-line medium"></div>
- <div class="pad1">
- <table class="coverage-summary">
- <thead>
- <tr>
- <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
- <th
- data-col="pic"
- data-type="number"
- data-fmt="html"
- data-html="true"
- class="pic"
- ></th>
- <th data-col="statements" data-type="number" data-fmt="pct" class="pct">
- Statements
- </th>
- <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
- <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
- <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
- <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
- <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td class="file empty" data-value="App.css"><a href="App.css.html">App.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="App.tsx"><a href="App.tsx.html">App.tsx</a></td>
- <td data-value="78.94" class="pic medium">
- <div class="chart">
- <div class="cover-fill" style="width: 78%"></div>
- <div class="cover-empty" style="width: 22%"></div>
- </div>
- </td>
- <td data-value="78.94" class="pct medium">78.94%</td>
- <td data-value="19" class="abs medium">15/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="77.77" class="pct medium">77.77%</td>
- <td data-value="9" class="abs medium">7/9</td>
- <td data-value="78.94" class="pct medium">78.94%</td>
- <td data-value="19" class="abs medium">15/19</td>
- </tr>
- </tbody>
- </table>
- </div>
- <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-13T21:49:58.924Z
</div>
- <script src="../prettify.js"></script>
- <script>
- window.onload = function () {
- prettyPrint();
- };
- </script>
- <script src="../sorter.js"></script>
- <script src="../block-navigation.js"></script>
- </body>
+ <div class='status-line medium'></div>
+ <div class="pad1">
+<table class="coverage-summary">
+<thead>
+<tr>
+ <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
+ <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
+ <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
+ <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
+ <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
+ <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
+ <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
+ <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
+</tr>
+</thead>
+<tbody><tr>
+ <td class="file empty" data-value="App.css"><a href="App.css.html">App.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="App.tsx"><a href="App.tsx.html">App.tsx</a></td>
+ <td data-value="72.72" class="pic medium">
+ <div class="chart"><div class="cover-fill" style="width: 72%"></div><div class="cover-empty" style="width: 28%"></div></div>
+ </td>
+ <td data-value="72.72" class="pct medium">72.72%</td>
+ <td data-value="22" class="abs medium">16/22</td>
+ <td data-value="62.5" class="pct medium">62.5%</td>
+ <td data-value="16" class="abs medium">10/16</td>
+ <td data-value="62.5" class="pct medium">62.5%</td>
+ <td data-value="8" class="abs medium">5/8</td>
+ <td data-value="72.72" class="pct medium">72.72%</td>
+ <td data-value="22" class="abs medium">16/22</td>
+ </tr>
+
+<tr>
+ <td class="file high" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
+ <td data-value="83.33" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 83%"></div><div class="cover-empty" style="width: 17%"></div></div>
+ </td>
+ <td data-value="83.33" class="pct high">83.33%</td>
+ <td data-value="12" class="abs high">10/12</td>
+ <td data-value="80" class="pct high">80%</td>
+ <td data-value="10" class="abs high">8/10</td>
+ <td data-value="100" class="pct high">100%</td>
+ <td data-value="2" class="abs high">2/2</td>
+ <td data-value="90.9" class="pct high">90.9%</td>
+ <td data-value="11" class="abs high">10/11</td>
+ </tr>
+
+</tbody>
+</table>
+</div>
+ <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-14T18:02:09.004Z
+ </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