From 1c73c999ac78d2e6d3a8c68b4e17058046326f55 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sat, 12 Jul 2025 14:55:19 +0900
Subject: feat(frontend): create pages and components

---
 frontend/src/components/FeedList.tsx | 150 +++++++++++++++++++++++++++++++++++
 1 file changed, 150 insertions(+)
 create mode 100644 frontend/src/components/FeedList.tsx

(limited to 'frontend/src/components/FeedList.tsx')

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>
+	);
+}
-- 
cgit v1.2.3-70-g09d2

