From e216c3bc97994b4172d15d52b46d5f6b75f35ea4 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Fri, 13 Feb 2026 22:01:12 +0900
Subject: feat: add feed sidebar and cursor-based pagination

Add a feed sidebar to /unread and /read pages for filtering articles by
feed, and replace the fixed 100-article limit with cursor-based
pagination using a "Load more" button.

Backend:
- Add PageInfo, ArticleConnection types and pagination args to GraphQL
- Replace GetUnreadArticles/GetReadArticles with parameterized queries
- Add GetFeedUnreadCounts query and composite index
- Add shared pagination helper in resolver

Frontend:
- Add FeedSidebar component with unread count badges
- Add usePaginatedArticles hook for cursor-based fetching
- Update ArticleList with Load more button and single-feed mode
- Use ?feed=<id> query parameter for feed filtering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 frontend/src/pages/UnreadArticles.tsx | 73 ++++++++++++++++++-----------------
 1 file changed, 38 insertions(+), 35 deletions(-)

(limited to 'frontend/src/pages/UnreadArticles.tsx')

diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx
index 28cc8b5..eade6fc 100644
--- a/frontend/src/pages/UnreadArticles.tsx
+++ b/frontend/src/pages/UnreadArticles.tsx
@@ -1,45 +1,48 @@
-import { useQuery } from "urql";
-import { ArticleList } from "../components";
-import { GetUnreadArticlesDocument } from "../graphql/generated/graphql";
-
-const urqlContextArticle = { additionalTypenames: ["Article"] };
+import { useSearch } from "wouter";
+import { ArticleList, FeedSidebar } from "../components";
+import { usePaginatedArticles } from "../hooks/usePaginatedArticles";
 
 export function UnreadArticles() {
-	const [{ data, fetching, error }] = useQuery({
-		query: GetUnreadArticlesDocument,
-		context: urqlContextArticle,
-	});
-
-	if (fetching) {
-		return (
-			<div className="py-8 text-center">
-				<p className="text-sm text-stone-400">Loading unread articles...</p>
-			</div>
-		);
-	}
+	const search = useSearch();
+	const params = new URLSearchParams(search);
+	const feedId = params.get("feed");
 
-	if (error) {
-		return (
-			<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
-				Error: {error.message}
-			</div>
-		);
-	}
+	const { articles, hasNextPage, loading, loadingMore, loadMore, error } =
+		usePaginatedArticles({ isReadView: false, feedId });
 
 	return (
-		<div>
-			<div className="mb-6">
-				<h1 className="text-xl font-semibold text-stone-900">Unread</h1>
-				{data?.unreadArticles && (
-					<p className="mt-1 text-sm text-stone-400">
-						{data.unreadArticles.length} article
-						{data.unreadArticles.length !== 1 ? "s" : ""} to read
-					</p>
+		<div className="flex gap-8">
+			<FeedSidebar basePath="/unread" />
+			<div className="min-w-0 flex-1">
+				<div className="mb-6">
+					<h1 className="text-xl font-semibold text-stone-900">Unread</h1>
+					{!loading && articles.length > 0 && (
+						<p className="mt-1 text-sm text-stone-400">
+							{articles.length}
+							{hasNextPage ? "+" : ""} article
+							{articles.length !== 1 ? "s" : ""} to read
+						</p>
+					)}
+				</div>
+				{loading ? (
+					<div className="py-8 text-center">
+						<p className="text-sm text-stone-400">Loading unread articles...</p>
+					</div>
+				) : error ? (
+					<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
+						Error: {error.message}
+					</div>
+				) : (
+					<ArticleList
+						articles={articles}
+						isReadView={false}
+						isSingleFeed={!!feedId}
+						hasNextPage={hasNextPage}
+						loadingMore={loadingMore}
+						onLoadMore={loadMore}
+					/>
 				)}
 			</div>
-			{data?.unreadArticles && (
-				<ArticleList articles={data.unreadArticles} isReadView={false} />
-			)}
 		</div>
 	);
 }
-- 
cgit v1.3-1-g0d28

