aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/FeedItem.tsx
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 08:58:38 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 08:58:38 -0800
commite3c379d069ffa9661561d25cdbf2f5894a2f8ee8 (patch)
tree24d0e9f5610dd9c8f873c5b78e6bc1c88d32840a /frontend/src/components/FeedItem.tsx
parent4b06155fbde91a1bef6361ef36efb28789861928 (diff)
downloadneko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.tar.gz
neko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.tar.bz2
neko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.zip
Refactor: project structure, implement dependency injection, and align v2 UI with v1
Diffstat (limited to 'frontend/src/components/FeedItem.tsx')
-rw-r--r--frontend/src/components/FeedItem.tsx146
1 files changed, 72 insertions, 74 deletions
diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx
index b86e60c..9b40114 100644
--- a/frontend/src/components/FeedItem.tsx
+++ b/frontend/src/components/FeedItem.tsx
@@ -3,86 +3,84 @@ import type { Item } from '../types';
import './FeedItem.css';
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]);
+ 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);
+ 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) => {
+ if (!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) => {
- if (!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>
- <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' }}>
- </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' }}></div>
+ </div>
+ {item.description && (
+ <div className="item-description" dangerouslySetInnerHTML={{ __html: item.description }} />
+ )}
+ </li>
+ );
}