From bd2508211760edbc1bad1d515587d08fd2ec99c9 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 06:58:30 -0800 Subject: Implement Item Interactions (read/star) with tests --- frontend/coverage/src/components/FeedItem.css.html | 325 +++++++++++++++++++ frontend/coverage/src/components/FeedItem.tsx.html | 352 +++++++++++++++++++++ .../coverage/src/components/FeedItems.css.html | 142 +-------- .../coverage/src/components/FeedItems.tsx.html | 44 +-- frontend/coverage/src/components/FeedList.css.html | 2 +- frontend/coverage/src/components/FeedList.tsx.html | 2 +- frontend/coverage/src/components/Login.css.html | 2 +- frontend/coverage/src/components/Login.tsx.html | 2 +- frontend/coverage/src/components/index.html | 52 ++- 9 files changed, 731 insertions(+), 192 deletions(-) create mode 100644 frontend/coverage/src/components/FeedItem.css.html create mode 100644 frontend/coverage/src/components/FeedItem.tsx.html (limited to 'frontend/coverage/src/components') diff --git a/frontend/coverage/src/components/FeedItem.css.html b/frontend/coverage/src/components/FeedItem.css.html new file mode 100644 index 0000000..420b55b --- /dev/null +++ b/frontend/coverage/src/components/FeedItem.css.html @@ -0,0 +1,325 @@ + + + + + + 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 +
+ + +
+

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

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
.feed-item {
+    border-bottom: 1px solid #f0f0f0;
+    padding: 1rem 0;
+    list-style: none;
+    /* Ensure no bullets if used in ul */
+}
+ 
+.feed-item.read .item-title {
+    color: #888;
+    font-weight: normal;
+}
+ 
+.feed-item.unread .item-title {
+    font-weight: bold;
+}
+ 
+.item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+}
+ 
+.item-title {
+    font-size: 1.2rem;
+    text-decoration: none;
+    color: #333;
+    display: block;
+    margin-bottom: 0.5rem;
+    flex: 1;
+    /* Take up remaining space */
+}
+ 
+.item-title:hover {
+    text-decoration: underline;
+    color: #007bff;
+}
+ 
+.item-actions {
+    display: flex;
+    gap: 0.5rem;
+    margin-left: 1rem;
+}
+ 
+.action-btn {
+    background: none;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    cursor: pointer;
+    padding: 2px 6px;
+    font-size: 1rem;
+    line-height: 1;
+}
+ 
+.action-btn:hover {
+    background-color: #f8f9fa;
+    border-color: #ccc;
+}
+ 
+.action-btn.is-starred {
+    color: gold;
+    border-color: gold;
+}
+ 
+.item-meta {
+    font-size: 0.85rem;
+    color: #666;
+    margin-bottom: 0.5rem;
+}
+ 
+.item-description {
+    color: #444;
+    line-height: 1.5;
+    font-size: 0.95rem;
+}
+ 
+.item-description img {
+    max-width: 100%;
+    height: auto;
+    display: block;
+    margin: 1rem 0;
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItem.tsx.html b/frontend/coverage/src/components/FeedItem.tsx.html new file mode 100644 index 0000000..418ab70 --- /dev/null +++ b/frontend/coverage/src/components/FeedItem.tsx.html @@ -0,0 +1,352 @@ + + + + + + Code coverage report for src/components/FeedItem.tsx + + + + + + + + + +
+
+

All files / src/components FeedItem.tsx

+
+ +
+ 94.73% + Statements + 18/19 +
+ + +
+ 91.66% + Branches + 22/24 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 94.73% + Lines + 18/19 +
+ + +
+

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

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90  +  +  +  +  +  +  +  +  +12x +12x +  +12x +2x +  +  +12x +1x +  +  +12x +3x +  +3x +3x +  +3x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +2x +  +  +  +  +2x +  +  +1x +  +1x +1x +  +  +  +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { useState } from 'react';
+import type { Item } from '../types';
+import './FeedItem.css';
+ 
+interface FeedItemProps {
+    item: Item;
+}
+ 
+export default function FeedItem({ item: initialItem }: FeedItemProps) {
+    const [item, setItem] = useState(initialItem);
+    const [loading, setLoading] = useState(false);
+ 
+    const toggleRead = () => {
+        updateItem({ ...item, read: !item.read });
+    };
+ 
+    const toggleStar = () => {
+        updateItem({ ...item, starred: !item.starred });
+    };
+ 
+    const updateItem = (newItem: Item) => {
+        setLoading(true);
+        // Optimistic update
+        const previousItem = item;
+        setItem(newItem);
+ 
+        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);
+            });
+    };
+ 
+    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>
+                <div className="item-actions">
+                    <button
+                        onClick={toggleRead}
+                        className={`action-btn ${item.read ? 'is-read' : 'is-unread'}`}
+                        title={item.read ? "Mark as unread" : "Mark as read"}
+                    >
+                        {item.read ? '📖' : 'uo'}
+                    </button>
+                    <button
+                        onClick={toggleStar}
+                        className={`action-btn ${item.starred ? 'is-starred' : 'is-unstarred'}`}
+                        title={item.starred ? "Unstar" : "Star"}
+                    >
+                        {item.starred ? '★' : '☆'}
+                    </button>
+                </div>
+            </div>
+            <div className="item-meta">
+                <span className="item-date">{new Date(item.publish_date).toLocaleDateString()}</span>
+                {item.feed_title && <span className="item-feed"> - {item.feed_title}</span>}
+            </div>
+            {item.description && (
+                <div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} />
+            )}
+        </li>
+    );
+}
+ 
+ +
+
+ + + + + + + + \ 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 3951ead..2140fe0 100644 --- a/frontend/coverage/src/components/FeedItems.css.html +++ b/frontend/coverage/src/components/FeedItems.css.html @@ -76,99 +76,7 @@ 11 12 13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  +14        @@ -194,52 +102,6 @@ .item-list { list-style: none; padding: 0; -} -  -.item { - border-bottom: 1px solid #f0f0f0; - padding: 1rem 0; -} -  -.item.read .item-title { - color: #888; - font-weight: normal; -} -  -.item.unread .item-title { - font-weight: bold; -} -  -.item-title { - font-size: 1.2rem; - text-decoration: none; - color: #333; - display: block; - margin-bottom: 0.5rem; -} -  -.item-title:hover { - text-decoration: underline; - color: #007bff; -} -  -.item-meta { - font-size: 0.85rem; - color: #666; - margin-bottom: 0.5rem; -} -  -.item-description { - color: #444; - line-height: 1.5; - font-size: 0.95rem; -} -  -.item-description img { - max-width: 100%; - height: auto; - display: block; - margin: 1rem 0; }
@@ -247,7 +109,7 @@