aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/coverage/src/components/Settings.tsx.html
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 10:03:35 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 10:03:35 -0800
commita4997a5fbc65913b55f2215eb3b868693bd76c51 (patch)
treefe303ee7c5e5aba89f1c13766b14556f6e3d2f79 /frontend/coverage/src/components/Settings.tsx.html
parent4d058d9ddb34f0e8d384b20d4b9e30f74d349129 (diff)
downloadneko-a4997a5fbc65913b55f2215eb3b868693bd76c51.tar.gz
neko-a4997a5fbc65913b55f2215eb3b868693bd76c51.tar.bz2
neko-a4997a5fbc65913b55f2215eb3b868693bd76c51.zip
test: increase frontend coverage for Settings and improve FeedItem css
Diffstat (limited to 'frontend/coverage/src/components/Settings.tsx.html')
-rw-r--r--frontend/coverage/src/components/Settings.tsx.html524
1 files changed, 352 insertions, 172 deletions
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