aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/FeedList.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-12 14:55:19 +0900
committernsfisis <nsfisis@gmail.com>2025-07-12 16:23:37 +0900
commit1c73c999ac78d2e6d3a8c68b4e17058046326f55 (patch)
tree41a01cc3827098dfc8d108e5b9cfac1fd5e7fb7f /frontend/src/components/FeedList.tsx
parent9da56e3023af305ba7c5fd49caab60ac8bb57100 (diff)
downloadfeedaka-1c73c999ac78d2e6d3a8c68b4e17058046326f55.tar.gz
feedaka-1c73c999ac78d2e6d3a8c68b4e17058046326f55.tar.zst
feedaka-1c73c999ac78d2e6d3a8c68b4e17058046326f55.zip
feat(frontend): create pages and components
Diffstat (limited to 'frontend/src/components/FeedList.tsx')
-rw-r--r--frontend/src/components/FeedList.tsx150
1 files changed, 150 insertions, 0 deletions
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
new file mode 100644
index 0000000..7e46e78
--- /dev/null
+++ b/frontend/src/components/FeedList.tsx
@@ -0,0 +1,150 @@
+import {
+ faCheckDouble,
+ faCircle,
+ faTrash,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useMutation, useQuery } from "urql";
+import {
+ GetFeedsDocument,
+ MarkFeedReadDocument,
+ MarkFeedUnreadDocument,
+ RemoveFeedDocument,
+} from "../graphql/generated/graphql";
+
+interface Props {
+ onFeedDeleted?: () => void;
+ selectedFeeds?: Set<string>;
+ onSelectFeed?: (feedId: string, selected: boolean) => void;
+}
+
+export function FeedList({
+ onFeedDeleted,
+ selectedFeeds,
+ onSelectFeed,
+}: Props) {
+ const [{ data, fetching, error }] = useQuery({
+ query: GetFeedsDocument,
+ });
+
+ const [, markFeedRead] = useMutation(MarkFeedReadDocument);
+ const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
+ const [, removeFeed] = useMutation(RemoveFeedDocument);
+
+ const handleMarkAllRead = async (feedId: string) => {
+ await markFeedRead({ id: feedId });
+ };
+
+ const handleMarkAllUnread = async (feedId: string) => {
+ await markFeedUnread({ id: feedId });
+ };
+
+ const handleDeleteFeed = async (feedId: string) => {
+ const confirmed = window.confirm(
+ "Are you sure you want to delete this feed?",
+ );
+ if (confirmed) {
+ await removeFeed({ id: feedId });
+ onFeedDeleted?.();
+ }
+ };
+
+ 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) => {
+ const unreadCount = feed.articles.filter((a) => !a.isRead).length;
+ const totalCount = feed.articles.length;
+
+ const isSelected = selectedFeeds?.has(feed.id) ?? false;
+
+ return (
+ <div
+ key={feed.id}
+ className={`rounded-lg border p-4 shadow-sm ${
+ isSelected
+ ? "border-blue-300 bg-blue-50"
+ : "border-gray-200 bg-white"
+ }`}
+ >
+ <div className="flex items-start justify-between">
+ {selectedFeeds && onSelectFeed && (
+ <div className="flex items-start gap-3">
+ <input
+ type="checkbox"
+ checked={isSelected}
+ onChange={(e) => onSelectFeed(feed.id, e.target.checked)}
+ className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+ <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">{feed.url}</p>
+ <div className="mt-2 flex items-center gap-4 text-sm">
+ <span className="text-gray-600">
+ {unreadCount} unread / {totalCount} total
+ </span>
+ <span className="text-gray-400">
+ Last fetched:{" "}
+ {new Date(feed.fetchedAt).toLocaleString()}
+ </span>
+ </div>
+ </div>
+ </div>
+ )}
+ {(!selectedFeeds || !onSelectFeed) && (
+ <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">{feed.url}</p>
+ <div className="mt-2 flex items-center gap-4 text-sm">
+ <span className="text-gray-600">
+ {unreadCount} unread / {totalCount} total
+ </span>
+ <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={faCheckDouble} />
+ </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={() => handleDeleteFeed(feed.id)}
+ className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700"
+ title="Delete feed"
+ >
+ <FontAwesomeIcon icon={faTrash} />
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+}