diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/ArticleItem.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/components/ArticleList.tsx | 71 | ||||
| -rw-r--r-- | frontend/src/components/FeedSidebar.tsx | 75 | ||||
| -rw-r--r-- | frontend/src/components/index.ts | 1 |
4 files changed, 145 insertions, 9 deletions
diff --git a/frontend/src/components/ArticleItem.tsx b/frontend/src/components/ArticleItem.tsx index f8fac24..dbdaf44 100644 --- a/frontend/src/components/ArticleItem.tsx +++ b/frontend/src/components/ArticleItem.tsx @@ -10,10 +10,9 @@ import { MarkArticleUnreadDocument, } from "../graphql/generated/graphql"; -type Article = NonNullable< - | GetUnreadArticlesQuery["unreadArticles"] - | GetReadArticlesQuery["readArticles"] ->[0]; +type Article = + | GetUnreadArticlesQuery["unreadArticles"]["articles"][number] + | GetReadArticlesQuery["readArticles"]["articles"][number]; interface Props { article: Article; 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> ); } 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> + ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 06f4c29..c0797b4 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,6 +1,7 @@ export { AddFeedForm } from "./AddFeedForm"; export { ArticleList } from "./ArticleList"; export { FeedList } from "./FeedList"; +export { FeedSidebar } from "./FeedSidebar"; export { Layout } from "./Layout"; export { MenuItem } from "./MenuItem"; export { Navigation } from "./Navigation"; |
