aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/ArticleList.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-13 22:01:12 +0900
committernsfisis <nsfisis@gmail.com>2026-02-13 22:01:12 +0900
commite216c3bc97994b4172d15d52b46d5f6b75f35ea4 (patch)
tree3ffbd74f4cb2d90846931c8dcbb97ec07f2b91f1 /frontend/src/components/ArticleList.tsx
parentc863e64c0521926e785f4aa7ecf4cf15bb9defa7 (diff)
downloadfeedaka-e216c3bc97994b4172d15d52b46d5f6b75f35ea4.tar.gz
feedaka-e216c3bc97994b4172d15d52b46d5f6b75f35ea4.tar.zst
feedaka-e216c3bc97994b4172d15d52b46d5f6b75f35ea4.zip
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>
Diffstat (limited to 'frontend/src/components/ArticleList.tsx')
-rw-r--r--frontend/src/components/ArticleList.tsx71
1 files changed, 66 insertions, 5 deletions
diff --git a/frontend/src/components/ArticleList.tsx b/frontend/src/components/ArticleList.tsx
index afadb25..ccf7826 100644
--- a/frontend/src/components/ArticleList.tsx
+++ b/frontend/src/components/ArticleList.tsx
@@ -5,15 +5,27 @@ import type {
} from "../graphql/generated/graphql";
import { ArticleItem } from "./ArticleItem";
+type ArticleType =
+ | GetUnreadArticlesQuery["unreadArticles"]["articles"]
+ | GetReadArticlesQuery["readArticles"]["articles"];
+
interface Props {
- articles: NonNullable<
- | GetUnreadArticlesQuery["unreadArticles"]
- | GetReadArticlesQuery["readArticles"]
- >;
+ articles: ArticleType;
isReadView?: boolean;
+ isSingleFeed?: boolean;
+ hasNextPage?: boolean;
+ loadingMore?: boolean;
+ onLoadMore?: () => void;
}
-export function ArticleList({ articles, isReadView }: Props) {
+export function ArticleList({
+ articles,
+ isReadView,
+ isSingleFeed,
+ hasNextPage,
+ loadingMore,
+ onLoadMore,
+}: Props) {
const [hiddenArticleIds, setHiddenArticleIds] = useState<Set<string>>(
new Set(),
);
@@ -36,6 +48,25 @@ export function ArticleList({ articles, isReadView }: Props) {
);
}
+ if (isSingleFeed) {
+ return (
+ <div className="space-y-1">
+ {visibleArticles.map((article) => (
+ <ArticleItem
+ key={article.id}
+ article={article}
+ onReadChange={handleArticleReadChange}
+ />
+ ))}
+ <LoadMoreButton
+ hasNextPage={hasNextPage}
+ loadingMore={loadingMore}
+ onLoadMore={onLoadMore}
+ />
+ </div>
+ );
+ }
+
// Group articles by feed
const articlesByFeed = visibleArticles.reduce(
(acc, article) => {
@@ -77,6 +108,36 @@ export function ArticleList({ articles, isReadView }: Props) {
</div>
</div>
))}
+ <LoadMoreButton
+ hasNextPage={hasNextPage}
+ loadingMore={loadingMore}
+ onLoadMore={onLoadMore}
+ />
+ </div>
+ );
+}
+
+function LoadMoreButton({
+ hasNextPage,
+ loadingMore,
+ onLoadMore,
+}: {
+ hasNextPage?: boolean;
+ loadingMore?: boolean;
+ onLoadMore?: () => void;
+}) {
+ if (!hasNextPage || !onLoadMore) return null;
+
+ return (
+ <div className="pt-4 text-center">
+ <button
+ type="button"
+ onClick={onLoadMore}
+ disabled={loadingMore}
+ className="rounded-lg bg-stone-100 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-200 disabled:opacity-50"
+ >
+ {loadingMore ? "Loading..." : "Load more"}
+ </button>
</div>
);
}