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.

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 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                      56x 56x   56x 22x     56x 1x     56x 1x   1x 1x   1x                       1x     1x         1x                   56x 1x 1x 1x   1x 1x     1x 1x               56x               1x 1x                                                            
import { useState, useEffect } from 'react';
import type { Item } from '../types';
import './FeedItem.css';
 
import { apiFetch } from '../utils';
 
interface FeedItemProps {
  item: Item;
}
 
export default function FeedItem({ item: initialItem }: FeedItemProps) {
  const [item, setItem] = useState(initialItem);
  const [loading, setLoading] = useState(false);
 
  useEffect(() => {
    setItem(initialItem);
  }, [initialItem]);
 
  const toggleStar = () => {
    updateItem({ ...item, starred: !item.starred });
  };
 
  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);
      });
  };
 
  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">
        <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>
  );
}