aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/components/FeedItem.tsx86
-rw-r--r--frontend/src/components/FeedList.tsx100
2 files changed, 101 insertions, 85 deletions
diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx
new file mode 100644
index 0000000..020825a
--- /dev/null
+++ b/frontend/src/components/FeedItem.tsx
@@ -0,0 +1,86 @@
+import { faCheck, faCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useMutation } from "urql";
+import type { GetFeedsQuery } from "../graphql/generated/graphql";
+import {
+ MarkFeedReadDocument,
+ MarkFeedUnreadDocument,
+ UnsubscribeFeedDocument,
+} from "../graphql/generated/graphql";
+
+type Feed = NonNullable<GetFeedsQuery["feeds"]>[0];
+
+interface Props {
+ feed: Feed;
+ onFeedUnsubscribed?: () => void;
+}
+
+export function FeedItem({ feed, onFeedUnsubscribed }: Props) {
+ const [, markFeedRead] = useMutation(MarkFeedReadDocument);
+ const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
+ const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument);
+
+ const handleMarkAllRead = async (feedId: string) => {
+ await markFeedRead({ id: feedId });
+ };
+
+ const handleMarkAllUnread = async (feedId: string) => {
+ await markFeedUnread({ id: feedId });
+ };
+
+ const handleUnsubscribeFeed = async (feedId: string) => {
+ const confirmed = window.confirm(
+ "Are you sure you want to unsubscribe from this feed?",
+ );
+ if (confirmed) {
+ await unsubscribeFeed({ id: feedId });
+ onFeedUnsubscribed?.();
+ }
+ };
+
+ return (
+ <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
+ <div className="flex items-start justify-between">
+ <div className="flex-1">
+ <h3 className="text-lg font-semibold text-gray-900">{feed.title}</h3>
+ <p className="mt-1 text-sm text-gray-500">
+ <a href={feed.url} target="_blank" rel="noreferrer">
+ {feed.url}
+ </a>
+ </p>
+ <div className="mt-2 flex items-center gap-4 text-sm">
+ <span className="text-gray-400">
+ Last fetched: {new Date(feed.fetchedAt).toLocaleString()}
+ </span>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ <button
+ type="button"
+ onClick={() => handleMarkAllRead(feed.id)}
+ className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
+ title="Mark all as read"
+ >
+ <FontAwesomeIcon icon={faCheck} />
+ </button>
+ <button
+ type="button"
+ onClick={() => handleMarkAllUnread(feed.id)}
+ className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
+ title="Mark all as unread"
+ >
+ <FontAwesomeIcon icon={faCircle} />
+ </button>
+ <button
+ type="button"
+ onClick={() => handleUnsubscribeFeed(feed.id)}
+ className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700"
+ title="Unsubscribe from feed"
+ >
+ <FontAwesomeIcon icon={faTrash} />
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index db12b13..d1d7bf3 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -1,12 +1,6 @@
-import { faCheck, faCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useMutation, useQuery } from "urql";
-import {
- GetFeedsDocument,
- MarkFeedReadDocument,
- MarkFeedUnreadDocument,
- UnsubscribeFeedDocument,
-} from "../graphql/generated/graphql";
+import { useQuery } from "urql";
+import { GetFeedsDocument } from "../graphql/generated/graphql";
+import { FeedItem } from "./FeedItem";
interface Props {
onFeedUnsubscribed?: () => void;
@@ -17,89 +11,25 @@ export function FeedList({ onFeedUnsubscribed }: Props) {
query: GetFeedsDocument,
});
- const [, markFeedRead] = useMutation(MarkFeedReadDocument);
- const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
- const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument);
-
- const handleMarkAllRead = async (feedId: string) => {
- await markFeedRead({ id: feedId });
- };
-
- const handleMarkAllUnread = async (feedId: string) => {
- await markFeedUnread({ id: feedId });
- };
-
- const handleUnsubscribeFeed = async (feedId: string) => {
- const confirmed = window.confirm(
- "Are you sure you want to unsubscribe from this feed?",
- );
- if (confirmed) {
- await unsubscribeFeed({ id: feedId });
- onFeedUnsubscribed?.();
- }
- };
-
- if (fetching) return <div className="p-4">Loading feeds...</div>;
- if (error)
+ if (fetching) {
+ return <div className="p-4">Loading feeds...</div>;
+ }
+ if (error) {
return <div className="p-4 text-red-600">Error: {error.message}</div>;
+ }
if (!data?.feeds || data.feeds.length === 0) {
return <div className="p-4 text-gray-500">No feeds added yet.</div>;
}
return (
<div className="space-y-4 p-4">
- {data.feeds.map((feed) => {
- return (
- <div
- key={feed.id}
- className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
- >
- <div className="flex items-start justify-between">
- <div className="flex-1">
- <h3 className="text-lg font-semibold text-gray-900">
- {feed.title}
- </h3>
- <p className="mt-1 text-sm text-gray-500">
- <a href={feed.url} target="_blank" rel="noreferrer">
- {feed.url}
- </a>
- </p>
- <div className="mt-2 flex items-center gap-4 text-sm">
- <span className="text-gray-400">
- Last fetched: {new Date(feed.fetchedAt).toLocaleString()}
- </span>
- </div>
- </div>
- <div className="flex items-center gap-2">
- <button
- type="button"
- onClick={() => handleMarkAllRead(feed.id)}
- className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
- title="Mark all as read"
- >
- <FontAwesomeIcon icon={faCheck} />
- </button>
- <button
- type="button"
- onClick={() => handleMarkAllUnread(feed.id)}
- className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
- title="Mark all as unread"
- >
- <FontAwesomeIcon icon={faCircle} />
- </button>
- <button
- type="button"
- onClick={() => handleUnsubscribeFeed(feed.id)}
- className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700"
- title="Unsubscribe from feed"
- >
- <FontAwesomeIcon icon={faTrash} />
- </button>
- </div>
- </div>
- </div>
- );
- })}
+ {data.feeds.map((feed) => (
+ <FeedItem
+ key={feed.id}
+ feed={feed}
+ onFeedUnsubscribed={onFeedUnsubscribed}
+ />
+ ))}
</div>
);
}