From a4997a5fbc65913b55f2215eb3b868693bd76c51 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 10:03:35 -0800 Subject: test: increase frontend coverage for Settings and improve FeedItem css --- frontend/coverage/src/components/FeedItem.css.html | 298 +++++----- frontend/coverage/src/components/FeedItem.tsx.html | 380 ++++++++----- .../coverage/src/components/FeedItems.css.html | 183 +++--- .../coverage/src/components/FeedItems.tsx.html | 597 ++++++++++---------- frontend/coverage/src/components/FeedList.css.html | 495 +++++++++++----- frontend/coverage/src/components/FeedList.tsx.html | 620 ++++++++++++++++----- frontend/coverage/src/components/Login.css.html | 137 ++--- frontend/coverage/src/components/Login.tsx.html | 225 ++++---- frontend/coverage/src/components/Settings.css.html | 453 +++++++++++---- frontend/coverage/src/components/Settings.tsx.html | 524 +++++++++++------ frontend/coverage/src/components/index.html | 504 ++++++++--------- 11 files changed, 2740 insertions(+), 1676 deletions(-) (limited to 'frontend/coverage/src/components') 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 @@ + - + + Code coverage report for src/components/FeedItem.css - - - - -
-
-

- All files / - src/components FeedItem.css -

-
-
- 0% - Statements - 0/0 -
- -
- 0% - Branches - 0/0 -
- -
- 0% - Functions - 0/0 -
- -
- 0% - Lines - 0/0 -
+ + + +
+
+

All files / src/components FeedItem.css

+
+ +
+ 0% + Statements + 0/0 +
+ + +
+ 0% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/0 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -175,7 +179,25 @@ 114 115 116 -117  +117 +118 +119 +120 +121 +122 +123 +124 +125 +126  +  +  +  +  +  +  +  +  +        @@ -292,139 +314,147 @@      
.feed-item {
-    padding: 1rem;
-    margin-top: 5rem;
-    list-style: none;
-    border-bottom: none;
+  padding: 1rem;
+  margin-top: 5rem;
+  list-style: none;
+  border-bottom: none;
 }
  
-.feed-item.read .item-title {
-    color: #888;
-    font-weight: normal;
-}
- 
-.feed-item.unread .item-title {
-    font-weight: bold;
-}
+/* removed read/unread specific font-weight to keep it always bold as requested */
  
 .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;
 }
  
 .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;
 }
  
 .item-title:hover {
-    text-decoration: none;
-    color: blue;
-    /* Legacy link color */
+  text-decoration: none;
+  color: var(--link-color);
 }
  
 .item-actions {
-    display: flex;
-    gap: 0.5rem;
-    margin-left: 1rem;
+  display: flex;
+  gap: 0.5rem;
+  margin-left: 1rem;
 }
  
 /* 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;
 }
  
 .star-btn.is-starred {
-    color: #ffd700;
-    /* Gold */
+  color: blue;
 }
  
 .star-btn.is-unstarred {
-    color: #ccc;
+  color: var(--text-color);
+  opacity: 0.3;
 }
  
 .star-btn:hover {
-    color: #ffeb3b;
+  color: blue;
 }
  
 .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;
 }
  
 .action-btn:hover {
-    background-color: #eee;
+  background-color: #eee;
 }
  
 .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;
 }
  
 .dateline a {
-    color: #ccc;
-    text-decoration: none;
+  color: #ccc;
+  text-decoration: none;
 }
  
 .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;
 }
  
 .item-description img {
-    max-width: 100%;
-    height: auto;
-    display: block;
-    margin: 1rem 0;
+  max-width: 100%;
+  height: auto;
+  display: block;
+  margin: 1rem 0;
 }
  
 .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;
+}
+ 
+.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;
+}
+ 
+.scrape-btn:hover {
+  background: var(--sidebar-bg);
 }
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/FeedItem.tsx - - - - -
-
-

- All files / - src/components FeedItem.tsx -

-
-
- 78.94% - Statements - 15/19 -
- -
- 88.88% - Branches - 16/18 -
- -
- 85.71% - Functions - 6/7 -
- -
- 78.94% - Lines - 15/19 -
+ + + +
+
+

All files / src/components FeedItem.tsx

+
+ +
+ 78.12% + Statements + 25/32 +
+ + +
+ 86.95% + Branches + 20/23 +
+ + +
+ 83.33% + Functions + 10/12 +
+ + +
+ 80.64% + Lines + 25/31 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -143,7 +147,41 @@ 82 83 84 -85  +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116  +  +  +        @@ -151,16 +189,18 @@       +33x +33x   -21x -21x +33x +16x     -21x +33x 1x     -21x +33x 1x   1x @@ -196,7 +236,28 @@       -21x +33x +1x +1x +1x +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +  +33x +  +  +        @@ -227,108 +288,143 @@       - 
import { useState } from 'react';
+ 
+ 
+ 
+ 
+ 
+ 
import { useState, useEffect } from 'react';
 import type { Item } from '../types';
 import './FeedItem.css';
  
+import { apiFetch } from '../utils';
+ 
 interface FeedItemProps {
-    item: Item;
+  item: Item;
 }
  
 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);
+ 
+  useEffect(() => {
+    setItem(initialItem);
+  }, [initialItem]);
  
+  const toggleStar = () => {
+    updateItem({ ...item, starred: !item.starred });
+  };
  
-    const toggleStar = () => {
-        updateItem({ ...item, starred: !item.starred });
-    };
+  const updateItem = (newItem: Item) => {
+    setLoading(true);
+    // Optimistic update
+    const previousItem = item;
+    setItem(newItem);
  
-    const updateItem = (newItem: Item) => {
-        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) => {
+        Iif (!res.ok) {
+          throw new Error('Failed to update item');
+        }
+        return res.json();
+      })
+      .then(() => {
+        // 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((err) => {
+        console.error('Error updating item:', err);
+        // Revert on error
+        setItem(previousItem);
+        setLoading(false);
+      });
+  };
  
-        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) => {
-                Iif (!res.ok) {
-                    throw new Error('Failed to update item');
-                }
-                return res.json();
-            })
-            .then(() => {
-                // 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((err) => {
-                console.error('Error updating item:', err);
-                // Revert on error
-                setItem(previousItem);
-                setLoading(false);
-            });
-    };
+  const loadFullContent = (e: React.MouseEvent) => {
+    e.stopPropagation();
+    setLoading(true);
+    apiFetch(`/api/item/${item._id}`)
+      .then((res) => {
+        Iif (!res.ok) throw new Error('Failed to fetch full content');
+        return res.json();
+      })
+      .then((data) => {
+        setItem({ ...item, ...data });
+        setLoading(false);
+      })
+      .catch((err) => {
+        console.error('Error fetching full content:', err);
+        setLoading(false);
+      });
+  };
  
-    return (
-        <li className={`feed-item ${item.read ? 'read' : 'unread'} ${loading ? 'loading' : ''}`}>
-            <div className="item-header">
-                <button
-                    onClick={(e) => {
-                        e.stopPropagation();
-                        toggleStar();
-                    }}
-                    className={`star-btn ${item.starred ? 'is-starred' : 'is-unstarred'}`}
-                    title={item.starred ? "Unstar" : "Star"}
-                >
-                    {item.starred ? '★' : '☆'}
-                </button>
-                <a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title">
-                    {item.title || '(No Title)'}
-                </a>
-            </div>
-            <div className="dateline">
-                <a href={item.url} target="_blank" rel="noopener noreferrer">
-                    {new Date(item.publish_date).toLocaleDateString()}
-                    {item.feed_title && ` - ${item.feed_title}`}
-                </a>
-                <div className="item-actions" style={{ display: 'inline-block', float: 'right' }}>
-                </div>
-            </div>
-            {item.description && (
-                <div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} />
-            )}
-        </li>
-    );
+  return (
+    <li className={`feed-item ${item.read ? 'read' : 'unread'} ${loading ? 'loading' : ''}`}>
+      <div className="item-header">
+        <a href={item.url} target="_blank" rel="noopener noreferrer" className="item-title">
+          {item.title || '(No Title)'}
+        </a>
+        <button
+          onClick={(e) => {
+            e.stopPropagation();
+            toggleStar();
+          }}
+          className={`star-btn ${item.starred ? 'is-starred' : 'is-unstarred'}`}
+          title={item.starred ? 'Unstar' : 'Star'}
+        >
+          ★
+        </button>
+      </div>
+      <div className="dateline">
+        <a href={item.url} target="_blank" rel="noopener noreferrer">
+          {new Date(item.publish_date).toLocaleDateString()}
+          {item.feed_title && ` - ${item.feed_title}`}
+        </a>
+        <div className="item-actions" style={{ display: 'inline-block', float: 'right' }}>
+          {!item.full_content && (
+            <button onClick={loadFullContent} className="scrape-btn" title="Load Full Content">
+              text
+            </button>
+          )}
+        </div>
+      </div>
+      {(item.full_content || item.description) && (
+        <div
+          className="item-description"
+          dangerouslySetInnerHTML={{ __html: item.full_content || item.description }}
+        />
+      )}
+    </li>
+  );
 }
  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/FeedItems.css - - - - -
-
-

- All files / - src/components FeedItems.css -

-
-
- 0% - Statements - 0/0 -
- -
- 0% - Branches - 0/0 -
- -
- 0% - Functions - 0/0 -
- -
- 0% - Lines - 0/0 -
+ + + +
+
+

All files / src/components FeedItems.css

+
+ +
+ 0% + Statements + 0/0 +
+ + +
+ 0% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/0 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -81,23 +85,7 @@ 20 21 22 -23 -24 -25 -26 -27 -28 -29 -30 -31  -  -  -  -  -  -  -  -  +23        @@ -120,53 +108,44 @@      
.feed-items {
-    padding: 1rem;
+  padding: 1rem 0;
+  /* Removing horizontal padding to avoid double-padding with FeedItem */
 }
  
 .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;
 }
  
 .item-list {
-    list-style: none;
-    padding: 0;
-}
- 
-.selected-item-container {
-    border-left: 4px solid #007bff;
-    background-color: #f8f9fa;
-    padding-left: 0.5rem;
-    margin-left: -0.5rem;
-    /* Compensate for padding/border to keep alignment */
-    transition: background-color 0.2s;
+  list-style: none;
+  padding: 0;
 }
  
 .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;
 }
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/FeedItems.tsx - - - - -
-
-

- All files / - src/components FeedItems.tsx -

-
-
- 89.34% - Statements - 109/122 -
- -
- 77.21% - Branches - 61/79 -
- -
- 86.2% - Functions - 25/29 -
- -
- 89.09% - Lines - 98/110 -
+ + + +
+
+

All files / src/components FeedItems.tsx

+
+ +
+ 88.97% + Statements + 113/127 +
+ + +
+ 75.3% + Branches + 61/81 +
+ + +
+ 86.2% + Functions + 25/29 +
+ + +
+ 88.69% + Lines + 102/115 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -282,7 +286,18 @@ 221 222 223 -224  +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234  +        @@ -298,6 +313,7 @@ 27x 27x 27x +27x   27x 8x @@ -323,6 +339,11 @@     8x +8x +  +  +  +8x   8x   @@ -330,6 +351,8 @@     8x +8x +      8x @@ -363,77 +386,79 @@   27x 7x +7x     -27x   27x -23x -3x -  -3x -2x 2x 2x 2x -2x -1x   +  +  +27x 2x   +3x +  2x   -1x -  -  -  -  +        -1x -1x -1x -1x   +27x 1x   +2x   +1x     -23x -23x +  +      27x +23x +3x +  +3x 2x 2x 2x -  -  -  -27x 2x -  -3x +2x +1x   2x   +2x   +1x +  +  +  +        +1x +1x +1x +1x   -27x 1x   -2x   -1x     +23x +23x +    -      27x @@ -477,7 +502,6 @@ 27x 14x   -  13x     @@ -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';   export default function FeedItems() { - const { feedId, tagName } = useParams<{ feedId: string; tagName: string }>(); - const [searchParams] = useSearchParams(); - const filterFn = searchParams.get('filter') || 'unread'; + const { feedId, tagName } = useParams<{ feedId: string; tagName: string }>(); + const [searchParams] = useSearchParams(); + const filterFn = searchParams.get('filter') || 'unread';   - const [items, setItems] = useState<Item[]>([]); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [error, setError] = useState(''); + const [items, setItems] = useState<Item[]>([]); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [error, setError] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(-1);   - const fetchItems = (maxId?: string) => { - if (maxId) { - setLoadingMore(true); - } else { - setLoading(true); - setItems([]); - } - setError(''); + const fetchItems = (maxId?: string) => { + if (maxId) { + setLoadingMore(true); + } else { + setLoading(true); + setItems([]); + } + setError('');   - let url = '/api/stream'; - const params = new URLSearchParams(); + let url = '/api/stream'; + const params = new URLSearchParams();   - 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); + }   - if (maxId) { - params.append('max_id', maxId); - } + if (maxId) { + params.append('max_id', maxId); + } +  + // Apply filters + const searchQuery = searchParams.get('q'); + Iif (searchQuery) { + params.append('q', searchQuery); + } +  + Iif (filterFn === 'all') { + params.append('read_filter', 'all'); + I} else if (filterFn === 'starred') { + params.append('starred', 'true'); + params.append('read_filter', 'all'); + } else { + // default to unread + Eif (!searchQuery) { + params.append('read_filter', 'unread'); + } + } +  + const queryString = params.toString(); + Eif (queryString) { + url += `?${queryString}`; + }   - // Apply filters - Iif (filterFn === 'all') { - params.append('read_filter', 'all'); - I} else if (filterFn === 'starred') { - params.append('starred', 'true'); - params.append('read_filter', 'all'); + apiFetch(url) + .then((res) => { + Iif (!res.ok) { + throw new Error('Failed to fetch items'); + } + return res.json(); + }) + .then((data) => { + if (maxId) { + setItems((prev) => [...prev, ...data]); } else { - // default to unread - params.append('read_filter', 'unread'); + setItems(data); } + setHasMore(data.length > 0); + setLoading(false); + setLoadingMore(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + setLoadingMore(false); + }); + };   - const queryString = params.toString(); - Eif (queryString) { - url += `?${queryString}`; - } + useEffect(() => { + fetchItems(); + setSelectedIndex(-1); + }, [feedId, tagName, filterFn, searchParams]);   - fetch(url) - .then((res) => { - Iif (!res.ok) { - throw new Error('Failed to fetch items'); - } - return res.json(); - }) - .then((data) => { - if (maxId) { - setItems((prev) => [...prev, ...data]); - } else { - setItems(data); - } - setHasMore(data.length > 0); - setLoading(false); - setLoadingMore(false); - }) - .catch((err) => { - setError(err.message); - setLoading(false); - setLoadingMore(false); - }); - };   - useEffect(() => { - fetchItems(); - }, [feedId, tagName, filterFn]); + const scrollToItem = (index: number) => { + const element = document.getElementById(`item-${index}`); + Eif (element) { + element.scrollIntoView({ behavior: 'auto', block: 'start' }); + } + };   - const [selectedIndex, setSelectedIndex] = useState(-1); + const markAsRead = (item: Item) => { + const updatedItem = { ...item, read: true }; + // Optimistic update + setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i)));   - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - Iif (items.length === 0) return; + apiFetch(`/api/item/${item._id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ read: true, starred: item.starred }), + }).catch((err) => console.error('Failed to mark read', err)); + };   - if (e.key === 'j') { - setSelectedIndex((prev) => { - const nextIndex = Math.min(prev + 1, items.length - 1); - Eif (nextIndex !== prev) { - const item = items[nextIndex]; - if (!item.read) { - markAsRead(item); - } - scrollToItem(nextIndex); - } - return nextIndex; - }); - I} else if (e.key === 'k') { - setSelectedIndex((prev) => { - const nextIndex = Math.max(prev - 1, 0); - if (nextIndex !== prev) { - scrollToItem(nextIndex); - } - return nextIndex; - }); - E} else if (e.key === 's') { - setSelectedIndex((currentIndex) => { - Eif (currentIndex >= 0 && currentIndex < items.length) { - toggleStar(items[currentIndex]); - } - return currentIndex; - }); - } - }; + const toggleStar = (item: Item) => { + const updatedItem = { ...item, starred: !item.starred }; + // Optimistic update + setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i)));   - window.addEventListener('keydown', handleKeyDown); - return () => 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((err) => console.error('Failed to toggle star', err)); + };   - const scrollToItem = (index: number) => { - const element = document.getElementById(`item-${index}`); - Eif (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }; + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + Iif (items.length === 0) return;   - const markAsRead = (item: Item) => { - const updatedItem = { ...item, read: true }; - // Optimistic update - setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); -  - fetch(`/api/item/${item._id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ read: true, starred: item.starred }), - }).catch((err) => console.error('Failed to mark read', err)); + if (e.key === 'j') { + setSelectedIndex((prev) => { + const nextIndex = Math.min(prev + 1, items.length - 1); + Eif (nextIndex !== prev) { + const item = items[nextIndex]; + if (!item.read) { + markAsRead(item); + } + scrollToItem(nextIndex); + } + return nextIndex; + }); + I} else if (e.key === 'k') { + setSelectedIndex((prev) => { + const nextIndex = Math.max(prev - 1, 0); + if (nextIndex !== prev) { + scrollToItem(nextIndex); + } + return nextIndex; + }); + E} else if (e.key === 's') { + setSelectedIndex((currentIndex) => { + Eif (currentIndex >= 0 && currentIndex < items.length) { + toggleStar(items[currentIndex]); + } + return currentIndex; + }); + } };   - const toggleStar = (item: Item) => { - const updatedItem = { ...item, starred: !item.starred }; - // Optimistic update - setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [items]);   - fetch(`/api/item/${item._id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ read: item.read, starred: !item.starred }), - }).catch((err) => console.error('Failed to toggle star', err)); - };   - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - // Infinity scroll sentinel - if (entry.target.id === 'load-more-sentinel') { - Eif (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) { - fetchItems(String(items[items.length - 1]._id)); - } - return; - }   - // If item is not intersecting and is above the viewport, it's been scrolled past - Eif (!entry.isIntersecting && entry.boundingClientRect.top < 0) { - const index = Number(entry.target.getAttribute('data-index')); - Eif (!isNaN(index) && index >= 0 && index < items.length) { - const item = items[index]; - Eif (!item.read) { - markAsRead(item); - } - } - } - }); - }, - { root: null, threshold: 0 } - ); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + // Infinity scroll sentinel + if (entry.target.id === 'load-more-sentinel') { + Eif (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) { + fetchItems(String(items[items.length - 1]._id)); + } + return; + }   - items.forEach((_, index) => { - const el = document.getElementById(`item-${index}`); - Eif (el) observer.observe(el); + // If item is not intersecting and is above the viewport, it's been scrolled past + Eif (!entry.isIntersecting && entry.boundingClientRect.top < 0) { + const index = Number(entry.target.getAttribute('data-index')); + Eif (!isNaN(index) && index >= 0 && index < items.length) { + const item = items[index]; + Eif (!item.read) { + markAsRead(item); + } + } + } }); + }, + { root: null, threshold: 0 } + );   - const sentinel = document.getElementById('load-more-sentinel'); - if (sentinel) observer.observe(sentinel); + items.forEach((_, index) => { + const el = document.getElementById(`item-${index}`); + Eif (el) observer.observe(el); + });   - return () => observer.disconnect(); - }, [items, loadingMore, hasMore]); + const sentinel = document.getElementById('load-more-sentinel'); + if (sentinel) observer.observe(sentinel);   - if (loading) return <div className="feed-items-loading">Loading items...</div>; - if (error) return <div className="feed-items-error">Error: {error}</div>; + return () => observer.disconnect(); + }, [items, loadingMore, hasMore]);   + if (loading) return <div className="feed-items-loading">Loading items...</div>; + if (error) return <div className="feed-items-error">Error: {error}</div>;   - return ( - <div className="feed-items"> - {items.length === 0 ? ( - <p>No items found.</p> - ) : ( - <ul className="item-list"> - {items.map((item, index) => ( - <div - id={`item-${index}`} - key={item._id} - data-index={index} - className={index === selectedIndex ? 'selected-item-container' : ''} - onClick={() => setSelectedIndex(index)} - > - <FeedItem item={item} /> - </div> - ))} - {hasMore && ( - <div id="load-more-sentinel" className="loading-more"> - {loadingMore ? 'Loading more...' : ''} - </div> - )} - </ul> - )} - </div> - ); + return ( + <div className="feed-items"> + {items.length === 0 ? ( + <p>No items found.</p> + ) : ( + <ul className="item-list"> + {items.map((item, index) => ( + <div + id={`item-${index}`} + key={item._id} + data-index={index} + data-selected={index === selectedIndex} + onClick={() => setSelectedIndex(index)} + > + <FeedItem item={item} /> + </div> + ))} + {hasMore && ( + <div id="load-more-sentinel" className="loading-more"> + {loadingMore ? 'Loading more...' : ''} + </div> + )} + </ul> + )} + </div> + ); }  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/FeedList.css - - - - -
-
-

- All files / - src/components FeedList.css -

-
-
- 0% - Statements - 0/0 -
- -
- 0% - Branches - 0/0 -
- -
- 0% - Functions - 0/0 -
- -
- 0% - Lines - 0/0 -
+ + + +
+
+

All files / src/components FeedList.css

+
+ +
+ 0% + Statements + 0/0 +
+ + +
+ 0% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/0 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -156,7 +160,159 @@ 95 96 97 -98  +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +        @@ -254,120 +410,195 @@      
.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;
 }
  
-.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? */
 }
  
-.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;
 }
  
-.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;
 }
  
-.feed-title {
-    color: #333;
-    text-decoration: none;
-    font-size: 0.9rem;
+.search-section {
+  margin-bottom: 1rem;
 }
  
-.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 */
 }
  
-.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;
+}
+ 
+.filter-list,
+.tag-list-items,
+.feed-list-items,
+.nav-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
 }
  
-.tag-section {
-    margin-top: 2rem;
+.filter-list li,
+.nav-list li {
+  margin-bottom: 0.1rem;
 }
  
+.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;
 }
  
+.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);
 }
  
-.filter-section {
-    margin-bottom: 2rem;
+.filter-list a.active,
+.tag-link.active,
+.feed-title.active {
+  font-weight: bold;
+  color: #000;
+  /* Active state black */
 }
  
-.filter-list {
-    display: block;
-    /* Stack vertically */
+.tag-item,
+.sidebar-feed-item {
+  margin-bottom: 0;
 }
  
-.filter-list {
-    display: block;
+.feed-category {
+  display: none;
 }
  
-.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;
 }
  
-.filter-list li a:hover {
-    color: blue;
-    background-color: transparent;
+.logout-link {
+  text-align: left;
+  width: 100%;
+  color: #777;
+  /* Make logout less prominent */
+}
+ 
+.logout-link:hover {
+  color: var(--link-color, blue);
+}
+ 
+.theme-section {
+  margin-top: 1rem;
+}
+ 
+.theme-selector {
+  display: flex;
+  gap: 0.5rem;
+}
+ 
+.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;
+}
+ 
+.theme-selector button.active {
+  background: var(--border-color, #999);
+  color: white;
+}
+ 
+/* Scrollbar styling for webkit */
+.dashboard-sidebar::-webkit-scrollbar {
+  width: 4px;
+}
+ 
+.dashboard-sidebar::-webkit-scrollbar-thumb {
+  background-color: var(--border-color, #999);
 }
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/FeedList.tsx - - - - -
-
-

- All files / - src/components FeedList.tsx -

-
-
- 91.66% - Statements - 22/24 -
- -
- 82.35% - Branches - 14/17 -
- -
- 100% - Functions - 8/8 -
- -
- 100% - Lines - 20/20 -
+ + + +
+
+

All files / src/components FeedList.tsx

+
+ +
+ 79.54% + Statements + 35/44 +
+ + +
+ 64.86% + Branches + 24/37 +
+ + +
+ 64.7% + Functions + 11/17 +
+ + +
+ 82.05% + Lines + 32/39 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -139,18 +143,153 @@ 78 79 80 -81  -  -  -  -  -  -11x -11x -11x -11x -  -11x +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +13x +13x +13x +13x +13x +13x +13x +13x +13x +13x +  +  +13x +  +  +13x +  +  +  +  +  +  +13x +2x +  +  +13x 6x   4x @@ -172,8 +311,56 @@       -11x -5x +13x +7x +  +6x +1x +  +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +    4x   @@ -191,7 +378,20 @@       -3x +  +  +  +  +  +  +2x +  +  +  +  +  +  +        @@ -207,7 +407,6 @@       -3x       @@ -216,107 +415,226 @@       +  +  +  +  +  +  +  +  +  +  +  +  +  +  +         
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';
+ 
+export default function FeedList({
+  theme,
+  setTheme,
+  setSidebarVisible,
+}: {
+  theme: string;
+  setTheme: (t: string) => void;
+  setSidebarVisible: (visible: boolean) => void;
+}) {
+  const [feeds, setFeeds] = useState<Feed[]>([]);
+  const [tags, setTags] = useState<Category[]>([]);
+  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();
+ 
+  const currentFilter =
+    searchParams.get('filter') ||
+    (location.pathname === '/' && !feedId && !tagName ? 'unread' : '');
+ 
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (searchQuery.trim()) {
+      navigate(`/?q=${encodeURIComponent(searchQuery.trim())}`);
+    }
+  };
+ 
+  const toggleFeeds = () => {
+    setFeedsExpanded(!feedsExpanded);
+  };
+ 
+  useEffect(() => {
+    Promise.all([
+      apiFetch('/api/feed/').then((res) => {
+        Iif (!res.ok) throw new Error('Failed to fetch feeds');
+        return res.json();
+      }),
+      apiFetch('/api/tag').then((res) => {
+        Iif (!res.ok) throw new Error('Failed to fetch tags');
+        return res.json();
+      }),
+    ])
+      .then(([feedsData, tagsData]) => {
+        setFeeds(feedsData);
+        setTags(tagsData);
+        setLoading(false);
+      })
+      .catch((err) => {
+        setError(err.message);
+        setLoading(false);
+      });
+  }, []);
  
-export default function FeedList() {
-    const [feeds, setFeeds] = useState<Feed[]>([]);
-    const [tags, setTags] = useState<Category[]>([]);
-    const [loading, setLoading] = useState(true);
-    const [error, setError] = useState('');
+  if (loading) return <div className="feed-list-loading">Loading feeds...</div>;
+  if (error) return <div className="feed-list-error">Error: {error}</div>;
  
-    useEffect(() => {
-        Promise.all([
-            fetch('/api/feed/').then(res => {
-                Iif (!res.ok) throw new Error('Failed to fetch feeds');
-                return res.json();
-            }),
-            fetch('/api/tag').then(res => {
-                Iif (!res.ok) throw new Error('Failed to fetch tags');
-                return res.json();
-            })
-        ])
-            .then(([feedsData, tagsData]) => {
-                setFeeds(feedsData);
-                setTags(tagsData);
-                setLoading(false);
-            })
-            .catch((err) => {
-                setError(err.message);
-                setLoading(false);
-            });
-    }, []);
+  const handleLogout = () => {
+    apiFetch('/api/logout', { method: 'POST' }).then(() => (window.location.href = '/v2/login'));
+  };
  
-    if (loading) return <div className="feed-list-loading">Loading feeds...</div>;
-    if (error) return <div className="feed-list-error">Error: {error}</div>;
+  return (
+    <div className="feed-list">
+      <h1 className="logo" onClick={() => setSidebarVisible(false)}>
+        🐱
+      </h1>
  
-    return (
-        <div className="feed-list">
-            <div className="filter-section">
-                <ul className="filter-list">
-                    <li><Link to="/?filter=unread">Unread</Link></li>
-                    <li><Link to="/?filter=all">All</Link></li>
-                    <li><Link to="/?filter=starred">Starred</Link></li>
-                </ul>
-            </div>
-            <div className="feed-section">
-                <h2>Feeds</h2>
-                {feeds.length === 0 ? (
-                    <p>No feeds found.</p>
-                ) : (
-                    <ul className="feed-list-items">
-                        {feeds.map((feed) => (
-                            <li key={feed._id} className="sidebar-feed-item">
-                                <Link to={`/feed/${feed._id}`} className="feed-title">
-                                    {feed.title || feed.url}
-                                </Link>
-                                {feed.category && <span className="feed-category">{feed.category}</span>}
-                            </li>
-                        ))}
-                    </ul>
-                )}
-            </div>
+      <div className="search-section">
+        <form onSubmit={handleSearch} className="search-form">
+          <input
+            type="search"
+            placeholder="search..."
+            value={searchQuery}
+            onChange={(e) => setSearchQuery(e.target.value)}
+            className="search-input"
+          />
+        </form>
+      </div>
  
-            {tags && tags.length > 0 && (
-                <div className="tag-section">
-                    <h2>Tags</h2>
-                    <ul className="tag-list-items">
-                        {tags.map((tag) => (
-                            <li key={tag.title} className="tag-item">
-                                <Link to={`/tag/${encodeURIComponent(tag.title)}`} className="tag-link">
-                                    {tag.title}
-                                </Link>
-                            </li>
-                        ))}
-                    </ul>
-                </div>
-            )}
+      <div className="filter-section">
+        <ul className="filter-list">
+          <li className="unread_filter">
+            <Link to="/?filter=unread" className={currentFilter === 'unread' ? 'active' : ''}>
+              unread
+            </Link>
+          </li>
+          <li className="all_filter">
+            <Link to="/?filter=all" className={currentFilter === 'all' ? 'active' : ''}>
+              all
+            </Link>
+          </li>
+          <li className="starred_filter">
+            <Link to="/?filter=starred" className={currentFilter === 'starred' ? 'active' : ''}>
+              starred
+            </Link>
+          </li>
+        </ul>
+      </div>
+ 
+      <div className="tag-section">
+        <h4 onClick={() => { }} className="section-header">
+          Tags
+        </h4>
+        <ul className="tag-list-items">
+          {tags.map((tag) => (
+            <li key={tag.title} className="tag-item">
+              <Link
+                to={`/tag/${encodeURIComponent(tag.title)}`}
+                className={`tag-link ${tagName === tag.title ? 'active' : ''}`}
+              >
+                {tag.title}
+              </Link>
+            </li>
+          ))}
+        </ul>
+      </div>
+ 
+      <div className="feed-section">
+        <h4 onClick={toggleFeeds} className="section-header">
+          Feeds
+        </h4>
+        {feedsExpanded &&
+          (feeds.length === 0 ? (
+            <p>No feeds found.</p>
+          ) : (
+            <ul className="feed-list-items">
+              {feeds.map((feed) => (
+                <li key={feed._id} className="sidebar-feed-item">
+                  <Link
+                    to={`/feed/${feed._id}`}
+                    className={`feed-title ${feedId === String(feed._id) ? 'active' : ''}`}
+                  >
+                    {feed.title || feed.url}
+                  </Link>
+                </li>
+              ))}
+            </ul>
+          ))}
+      </div>
+ 
+      <div className="nav-section">
+        <ul className="nav-list">
+          <li>
+            <Link to="/settings" className="nav-link">
+              settings
+            </Link>
+          </li>
+          <li>
+            <button onClick={handleLogout} className="logout-link">
+              logout
+            </button>
+          </li>
+        </ul>
+      </div>
+ 
+      <div className="theme-section">
+        <div className="theme-selector">
+          <button
+            onClick={() => setTheme('light')}
+            className={theme === 'light' ? 'active' : ''}
+            title="Light Theme"
+          >
+            ☀️
+          </button>
+          <button
+            onClick={() => setTheme('dark')}
+            className={theme === 'dark' ? 'active' : ''}
+            title="Dark Theme"
+          >
+            🌙
+          </button>
         </div>
-    );
+      </div>
+    </div>
+  );
 }
  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/Login.css - - - - -
-
-

- All files / - src/components Login.css -

-
-
- 0% - Statements - 0/0 -
- -
- 0% - Branches - 0/0 -
- -
- 0% - Functions - 0/0 -
- -
- 0% - Lines - 0/0 -
+ + + +
+
+

All files / src/components Login.css

+
+ +
+ 0% + Statements + 0/0 +
+ + +
+ 0% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/0 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -233,7 +237,7 @@ text-align: center; }   -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; }   -button[type="submit"]:hover { +button[type='submit']:hover { background-color: #0056b3; }  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/Login.tsx - - - - -
-
-

- All files / - src/components Login.tsx -

-
-
- 100% - Statements - 17/17 -
- -
- 83.33% - Branches - 5/6 -
- -
- 100% - Functions - 3/3 -
- -
- 100% - Lines - 17/17 -
+ + + +
+
+

All files / src/components Login.tsx

+
+ +
+ 100% + Statements + 17/17 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 17/17 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -113,7 +117,11 @@ 52 53 54 -55  +55 +56 +57  +  +        @@ -171,74 +179,75 @@ import { useNavigate } from 'react-router-dom'; import './Login.css';   +import { apiFetch } from '../utils'; +  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();   - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError('');   - 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);   - const res = await fetch('/api/login', { - method: 'POST', - body: params, - }); + const res = await apiFetch('/api/login', { + method: 'POST', + body: params, + });   - if (res.ok) { - navigate('/'); - } else { - const data = await res.json(); - setError(data.message || 'Login failed'); - } - } catch (err) { - setError('Network error'); - } - }; + if (res.ok) { + navigate('/'); + } else { + const data = await res.json(); + setError(data.message || 'Login failed'); + } + } catch (err) { + setError('Network error'); + } + };   - return ( - <div className="login-container"> - <form onSubmit={handleSubmit} className="login-form"> - <h1>neko rss mode</h1> - <div className="form-group"> - <label htmlFor="password">password</label> - <input - id="password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - autoFocus - /> - </div> - {error && <div className="error-message">{error}</div>} - <button type="submit">login</button> - </form> + return ( + <div className="login-container"> + <form onSubmit={handleSubmit} className="login-form"> + <h1>neko rss mode</h1> + <div className="form-group"> + <label htmlFor="password">password</label> + <input + id="password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + autoFocus + /> </div> - ); + {error && <div className="error-message">{error}</div>} + <button type="submit">login</button> + </form> + </div> + ); }  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/Settings.css - - - - -
-
-

- All files / - src/components Settings.css -

-
-
- 0% - Statements - 0/0 -
- -
- 0% - Branches - 0/0 -
- -
- 0% - Functions - 0/0 -
- -
- 0% - Lines - 0/0 -
+ + + +
+
+

All files / src/components Settings.css

+
+ +
+ 0% + Statements + 0/0 +
+ + +
+ 0% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/0 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -141,7 +145,163 @@ 80 81 82 -83  +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +        @@ -224,105 +384,182 @@      
.settings-page {
-    padding: 2rem;
-    max-width: 800px;
-    margin: 0 auto;
+  padding: 2rem;
+  max-width: 800px;
+  margin: 0 auto;
 }
  
 .add-feed-section {
-    background: #f9f9f9;
-    padding: 1.5rem;
-    border-radius: 8px;
-    margin-bottom: 2rem;
-    border: 1px solid #eee;
+  background: var(--sidebar-bg);
+  padding: 1.5rem;
+  border-radius: 8px;
+  margin-bottom: 2rem;
+  border: 1px solid var(--border-color);
 }
  
 .add-feed-form {
-    display: flex;
-    gap: 1rem;
+  display: flex;
+  gap: 1rem;
 }
  
 .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;
 }
  
 .error-message {
-    color: #d32f2f;
-    margin-top: 1rem;
+  color: #d32f2f;
+  margin-top: 1rem;
 }
  
 .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;
 }
  
 .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);
 }
  
 .settings-feed-item:last-child {
-    border-bottom: none;
+  border-bottom: none;
 }
  
 .feed-info {
-    display: flex;
-    flex-direction: column;
+  display: flex;
+  flex-direction: column;
 }
  
 .feed-title {
-    font-weight: bold;
-    font-size: 1.1rem;
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  font-weight: bold;
+  font-size: 1.1rem;
 }
  
 .feed-url {
-    color: #666;
-    font-size: 0.9rem;
+  color: var(--text-color);
+  opacity: 0.6;
+  font-size: 0.9rem;
 }
  
 .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;
 }
  
 .delete-btn:hover {
-    background: #ff1744;
+  background: #ff1744;
 }
  
 .delete-btn:disabled {
-    background: #ffcdd2;
-    cursor: not-allowed;
+  background: #ffcdd2;
+  cursor: not-allowed;
+}
+ 
+.import-export-section {
+  display: flex;
+  gap: 2rem;
+  margin-bottom: 2rem;
+}
+ 
+@media (max-width: 600px) {
+  .import-export-section {
+    flex-direction: column;
+  }
+}
+ 
+.import-section,
+.export-section {
+  flex: 1;
+  background: var(--sidebar-bg);
+  padding: 1.5rem;
+  border-radius: 8px;
+  border: 1px solid var(--border-color);
+}
+ 
+.import-form {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+ 
+.file-input {
+  font-size: 0.9rem;
+  max-width: 100%;
+}
+ 
+.export-buttons {
+  display: flex;
+  gap: 1rem;
+  flex-wrap: wrap;
+}
+ 
+.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;
+}
+ 
+.export-btn:hover {
+  background: var(--sidebar-bg);
+}
+ 
+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;
+}
+ 
+button:hover:not(:disabled) {
+  background: var(--sidebar-bg);
+}
+ 
+button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
 }
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components/Settings.tsx - - - - -
-
-

- All files / - src/components Settings.tsx -

-
-
- 75.55% - Statements - 34/45 -
- -
- 56.25% - Branches - 9/16 -
- -
- 82.35% - Functions - 14/17 -
- -
- 84.61% - Lines - 33/39 -
+ + + +
+
+

All files / src/components Settings.tsx

+
+ +
+ 56.25% + Statements + 36/64 +
+ + +
+ 41.66% + Branches + 10/24 +
+ + +
+ 63.63% + Functions + 14/22 +
+ + +
+ 62.5% + Lines + 35/56 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-

+    
+    
+
1 2 3 @@ -180,7 +184,67 @@ 119 120 121 -122  +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181  +        @@ -191,8 +255,6 @@ 14x   14x -3x -    14x 4x @@ -212,6 +274,10 @@     14x +3x +  +  +14x 1x 1x   @@ -254,6 +320,34 @@     14x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +14x       @@ -281,6 +375,34 @@       +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  5x     @@ -304,142 +426,200 @@  
import React, { useEffect, useState } from 'react';
 import type { Feed } from '../types';
 import './Settings.css';
+import { apiFetch } from '../utils';
  
 export default function Settings() {
-    const [feeds, setFeeds] = useState<Feed[]>([]);
-    const [newFeedUrl, setNewFeedUrl] = useState('');
-    const [loading, setLoading] = useState(false);
-    const [error, setError] = useState<string | null>(null);
+  const [feeds, setFeeds] = useState<Feed[]>([]);
+  const [newFeedUrl, setNewFeedUrl] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
  
-    useEffect(() => {
+  const [importFile, setImportFile] = useState<File | null>(null);
+ 
+  const fetchFeeds = () => {
+    setLoading(true);
+    apiFetch('/api/feed/')
+      .then((res) => {
+        Iif (!res.ok) throw new Error('Failed to fetch feeds');
+        return res.json();
+      })
+      .then((data) => {
+        setFeeds(data);
+        setLoading(false);
+      })
+      .catch((err) => {
+        setError(err.message);
+        setLoading(false);
+      });
+  };
+ 
+  useEffect(() => {
+    fetchFeeds();
+  }, []);
+ 
+  const handleAddFeed = (e: React.FormEvent) => {
+    e.preventDefault();
+    Iif (!newFeedUrl) return;
+ 
+    setLoading(true);
+    apiFetch('/api/feed/', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ url: newFeedUrl }),
+    })
+      .then((res) => {
+        Iif (!res.ok) throw new Error('Failed to add feed');
+        return res.json();
+      })
+      .then(() => {
+        setNewFeedUrl('');
         fetchFeeds();
-    }, []);
+      })
+      .catch((err) => {
+        setError(err.message);
+        setLoading(false);
+      });
+  };
+ 
+  const handleDeleteFeed = (id: number) => {
+    Iif (!globalThis.confirm('Are you sure you want to delete this feed?')) return;
  
-    const fetchFeeds = () => {
-        setLoading(true);
-        fetch('/api/feed/')
-            .then((res) => {
-                Iif (!res.ok) throw new Error('Failed to fetch feeds');
-                return res.json();
-            })
-            .then((data) => {
-                setFeeds(data);
-                setLoading(false);
-            })
-            .catch((err) => {
-                setError(err.message);
-                setLoading(false);
-            });
-    };
+    setLoading(true);
+    apiFetch(`/api/feed/${id}`, {
+      method: 'DELETE',
+    })
+      .then((res) => {
+        Iif (!res.ok) throw new Error('Failed to delete feed');
+        setFeeds(feeds.filter((f) => f._id !== id));
+        setLoading(false);
+      })
+      .catch((err) => {
+        setError(err.message);
+        setLoading(false);
+      });
+  };
  
-    const handleAddFeed = (e: React.FormEvent) => {
-        e.preventDefault();
-        Iif (!newFeedUrl) return;
+  const handleImport = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!importFile) return;
  
-        setLoading(true);
-        fetch('/api/feed/', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ url: newFeedUrl }),
-        })
-            .then((res) => {
-                Iif (!res.ok) throw new Error('Failed to add feed');
-                return res.json();
-            })
-            .then(() => {
-                setNewFeedUrl('');
-                fetchFeeds(); // Refresh list (or we could append if server returns full feed object)
-            })
-            .catch((err) => {
-                setError(err.message);
-                setLoading(false);
-            });
-    };
+    setLoading(true);
+    const formData = new FormData();
+    formData.append('file', importFile);
+    formData.append('format', 'opml');
  
-    const handleDeleteFeed = (id: number) => {
-        Iif (!globalThis.confirm('Are you sure you want to delete this feed?')) return;
+    apiFetch('/api/import', {
+      method: 'POST',
+      body: formData,
+    })
+      .then((res) => {
+        if (!res.ok) throw new Error('Failed to import feeds');
+        return res.json();
+      })
+      .then(() => {
+        setImportFile(null);
+        fetchFeeds();
+        alert('Import successful!');
+      })
+      .catch((err) => {
+        setError(err.message);
+        setLoading(false);
+      });
+  };
  
-        setLoading(true);
-        fetch(`/api/feed/${id}`, {
-            method: 'DELETE',
-        })
-            .then((res) => {
-                Iif (!res.ok) throw new Error('Failed to delete feed');
-                setFeeds(feeds.filter((f) => f._id !== id));
-                setLoading(false);
-            })
-            .catch((err) => {
-                setError(err.message);
-                setLoading(false);
-            });
-    };
+  return (
+    <div className="settings-page">
+      <h2>Settings</h2>
  
-    return (
-        <div className="settings-page">
-            <h2>Settings</h2>
+      <div className="add-feed-section">
+        <h3>Add New Feed</h3>
+        <form onSubmit={handleAddFeed} className="add-feed-form">
+          <input
+            type="url"
+            value={newFeedUrl}
+            onChange={(e) => setNewFeedUrl(e.target.value)}
+            placeholder="https://example.com/feed.xml"
+            required
+            className="feed-input"
+            disabled={loading}
+          />
+          <button type="submit" disabled={loading}>
+            Add Feed
+          </button>
+        </form>
+      </div>
  
-            <div className="add-feed-section">
-                <h3>Add New Feed</h3>
-                <form onSubmit={handleAddFeed} className="add-feed-form">
-                    <input
-                        type="url"
-                        value={newFeedUrl}
-                        onChange={(e) => setNewFeedUrl(e.target.value)}
-                        placeholder="https://example.com/feed.xml"
-                        required
-                        className="feed-input"
-                        disabled={loading}
-                    />
-                    <button type="submit" disabled={loading}>
-                        Add Feed
-                    </button>
-                </form>
-                {error && <p className="error-message">{error}</p>}
-            </div>
+      <div className="import-export-section">
+        <div className="import-section">
+          <h3>Import Feeds (OPML)</h3>
+          <form onSubmit={handleImport} className="import-form">
+            <input
+              type="file"
+              accept=".opml,.xml,.txt"
+              onChange={(e) => setImportFile(e.target.files?.[0] || null)}
+              className="file-input"
+              disabled={loading}
+            />
+            <button type="submit" disabled={!importFile || loading}>
+              Import
+            </button>
+          </form>
+        </div>
  
-            <div className="feed-list-section">
-                <h3>Manage Feeds</h3>
-                {loading && <p>Loading...</p>}
-                <ul className="settings-feed-list">
-                    {feeds.map((feed) => (
-                        <li key={feed._id} className="settings-feed-item">
-                            <div className="feed-info">
-                                <span className="feed-title">{feed.title || '(No Title)'}</span>
-                                <span className="feed-url">{feed.url}</span>
-                            </div>
-                            <button
-                                onClick={() => handleDeleteFeed(feed._id)}
-                                className="delete-btn"
-                                disabled={loading}
-                                title="Delete Feed"
-                            >
-                                Delete
-                            </button>
-                        </li>
-                    ))}
-                </ul>
-            </div>
+        <div className="export-section">
+          <h3>Export Feeds</h3>
+          <div className="export-buttons">
+            <a href="/api/export/opml" className="export-btn">OPML</a>
+            <a href="/api/export/text" className="export-btn">Text</a>
+            <a href="/api/export/json" className="export-btn">JSON</a>
+          </div>
         </div>
-    );
+      </div>
+ 
+      {error && <p className="error-message">{error}</p>}
+ 
+      <div className="feed-list-section">
+        <h3>Manage Feeds</h3>
+        {loading && <p>Loading...</p>}
+        <ul className="settings-feed-list">
+          {feeds.map((feed) => (
+            <li key={feed._id} className="settings-feed-item">
+              <div className="feed-info">
+                <span className="feed-title">{feed.title || '(No Title)'}</span>
+                <span className="feed-url">{feed.url}</span>
+              </div>
+              <button
+                onClick={() => handleDeleteFeed(feed._id)}
+                className="delete-btn"
+                disabled={loading}
+                title="Delete Feed"
+              >
+                Delete
+              </button>
+            </li>
+          ))}
+        </ul>
+      </div>
+    </div>
+  );
 }
  
-
- -
- - - - - - - +
+
+ + + + + + + \ 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 @@ + - + + Code coverage report for src/components - - - - -
-
+ + + +
+

All files src/components

-
-
- 86.78% - Statements - 197/227 -
- -
- 77.2% - Branches - 105/136 -
- -
- 87.5% - Functions - 56/64 -
- -
- 89.26% - Lines - 183/205 -
+
+ +
+ 79.57% + Statements + 226/284 +
+ + +
+ 70.17% + Branches + 120/171 +
+ + +
+ 75.9% + Functions + 63/83 +
+ + +
+ 81.78% + Lines + 211/258 +
+ +

- Press n or j to go to the next uncovered block, b, - p or k for the previous block. + Press n or j to go to the next uncovered block, b, p or k for the previous block.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+
+
File - Statements - BranchesFunctionsLines
- FeedItem.css - -
-
-
-
-
0%0/00%0/00%0/00%0/0
+ + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
FeedItem.css +
+
0%0/00%0/00%0/00%0/0
- FeedItem.tsx - -
-
-
-
-
78.94%15/1988.88%16/1885.71%6/778.94%15/19
FeedItem.tsx +
+
78.12%25/3286.95%20/2383.33%10/1280.64%25/31
- FeedItems.css - -
-
-
-
-
0%0/00%0/00%0/00%0/0
FeedItems.css +
+
0%0/00%0/00%0/00%0/0
- FeedItems.tsx - -
-
-
-
-
89.34%109/12277.21%61/7986.2%25/2989.09%98/110
FeedItems.tsx +
+
88.97%113/12775.3%61/8186.2%25/2988.69%102/115
- FeedList.css - -
-
-
-
-
0%0/00%0/00%0/00%0/0
FeedList.css +
+
0%0/00%0/00%0/00%0/0
- FeedList.tsx - -
-
-
-
-
91.66%22/2482.35%14/17100%8/8100%20/20
FeedList.tsx +
+
79.54%35/4464.86%24/3764.7%11/1782.05%32/39
- Login.css - -
-
-
-
-
0%0/00%0/00%0/00%0/0
Login.css +
+
0%0/00%0/00%0/00%0/0
- Login.tsx - -
-
-
-
-
100%17/1783.33%5/6100%3/3100%17/17
Login.tsx +
+
100%17/1783.33%5/6100%3/3100%17/17
- Settings.css - -
-
-
-
-
0%0/00%0/00%0/00%0/0
Settings.css +
+
0%0/00%0/00%0/00%0/0
- Settings.tsx - -
-
-
-
-
75.55%34/4556.25%9/1682.35%14/1784.61%33/39
-
-
- -
- - - - - - - + + Settings.tsx + +
+ + 56.25% + 36/64 + 41.66% + 10/24 + 63.63% + 14/22 + 62.5% + 35/56 + + + + +
+
+
+ + + + + + + \ No newline at end of file -- cgit v1.2.3