diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-13 22:01:12 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-13 22:01:12 +0900 |
| commit | e216c3bc97994b4172d15d52b46d5f6b75f35ea4 (patch) | |
| tree | 3ffbd74f4cb2d90846931c8dcbb97ec07f2b91f1 /frontend/src/components/FeedSidebar.tsx | |
| parent | c863e64c0521926e785f4aa7ecf4cf15bb9defa7 (diff) | |
| download | feedaka-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/FeedSidebar.tsx')
| -rw-r--r-- | frontend/src/components/FeedSidebar.tsx | 75 |
1 files changed, 75 insertions, 0 deletions
diff --git a/frontend/src/components/FeedSidebar.tsx b/frontend/src/components/FeedSidebar.tsx new file mode 100644 index 0000000..73d9504 --- /dev/null +++ b/frontend/src/components/FeedSidebar.tsx @@ -0,0 +1,75 @@ +import { useQuery } from "urql"; +import { useLocation, useSearch } from "wouter"; +import { GetFeedsDocument } from "../graphql/generated/graphql"; + +interface Props { + basePath: string; +} + +const urqlContextFeed = { additionalTypenames: ["Feed", "Article"] }; + +export function FeedSidebar({ basePath }: Props) { + const search = useSearch(); + const [, setLocation] = useLocation(); + const params = new URLSearchParams(search); + const selectedFeedId = params.get("feed"); + + const [{ data, fetching }] = useQuery({ + query: GetFeedsDocument, + context: urqlContextFeed, + }); + + const handleSelect = (feedId: string | null) => { + if (feedId) { + setLocation(`${basePath}?feed=${feedId}`); + } else { + setLocation(basePath); + } + }; + + return ( + <nav className="w-56 shrink-0"> + <h2 className="mb-3 text-xs font-semibold uppercase tracking-wide text-stone-400"> + Feeds + </h2> + <ul className="space-y-0.5"> + <li> + <button + type="button" + onClick={() => handleSelect(null)} + className={`w-full rounded-md px-3 py-1.5 text-left text-sm transition-colors ${ + !selectedFeedId + ? "bg-stone-200 font-medium text-stone-900" + : "text-stone-600 hover:bg-stone-100" + }`} + > + All feeds + </button> + </li> + {fetching && ( + <li className="px-3 py-1.5 text-xs text-stone-400">Loading...</li> + )} + {data?.feeds.map((feed) => ( + <li key={feed.id}> + <button + type="button" + onClick={() => handleSelect(feed.id)} + className={`flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-sm transition-colors ${ + selectedFeedId === feed.id + ? "bg-stone-200 font-medium text-stone-900" + : "text-stone-600 hover:bg-stone-100" + }`} + > + <span className="min-w-0 truncate">{feed.title}</span> + {feed.unreadCount > 0 && ( + <span className="ml-2 shrink-0 rounded-full bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-700"> + {feed.unreadCount} + </span> + )} + </button> + </li> + ))} + </ul> + </nav> + ); +} |
