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 | 33x 33x 33x 16x 33x 1x 33x 1x 1x 1x 1x 1x 1x 1x 33x 1x 1x 1x 1x 1x 1x 1x 33x 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>
);
}
|