aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ArticleItem.tsx7
-rw-r--r--frontend/src/components/ArticleList.tsx71
-rw-r--r--frontend/src/components/FeedSidebar.tsx75
-rw-r--r--frontend/src/components/index.ts1
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";