aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/ArticleItem.tsx86
-rw-r--r--frontend/src/components/ArticleList.tsx82
2 files changed, 91 insertions, 77 deletions
diff --git a/frontend/src/components/ArticleItem.tsx b/frontend/src/components/ArticleItem.tsx
new file mode 100644
index 0000000..e5a506f
--- /dev/null
+++ b/frontend/src/components/ArticleItem.tsx
@@ -0,0 +1,86 @@
+import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useMutation } from "urql";
+import type {
+ GetReadArticlesQuery,
+ GetUnreadArticlesQuery,
+} from "../graphql/generated/graphql";
+import {
+ MarkArticleReadDocument,
+ MarkArticleUnreadDocument,
+} from "../graphql/generated/graphql";
+
+type Article = NonNullable<
+ | GetUnreadArticlesQuery["unreadArticles"]
+ | GetReadArticlesQuery["readArticles"]
+>[0];
+
+interface Props {
+ article: Article;
+ showReadStatus?: boolean;
+}
+
+export function ArticleItem({ article, showReadStatus = true }: Props) {
+ const [, markArticleRead] = useMutation(MarkArticleReadDocument);
+ const [, markArticleUnread] = useMutation(MarkArticleUnreadDocument);
+
+ const handleToggleRead = async (
+ articleId: string,
+ isCurrentlyRead: boolean,
+ ) => {
+ if (isCurrentlyRead) {
+ await markArticleUnread({ id: articleId });
+ } else {
+ await markArticleRead({ id: articleId });
+ }
+ };
+
+ const handleArticleClick = async (article: Article) => {
+ // Open article in new tab and mark as read if it's unread
+ window.open(article.url, "_blank", "noreferrer");
+ if (!article.isRead) {
+ await markArticleRead({ id: article.id });
+ }
+ };
+
+ return (
+ <div
+ className={`group flex items-center gap-3 rounded-lg border p-3 hover:bg-gray-50 ${
+ article.isRead
+ ? "border-gray-200 bg-white"
+ : "border-blue-200 bg-blue-50"
+ }`}
+ >
+ {showReadStatus && (
+ <button
+ type="button"
+ onClick={() => handleToggleRead(article.id, article.isRead)}
+ className={`flex-shrink-0 rounded p-1 transition-colors ${
+ article.isRead
+ ? "text-gray-400 hover:text-gray-600"
+ : "text-blue-600 hover:text-blue-700"
+ }`}
+ title={article.isRead ? "Mark as unread" : "Mark as read"}
+ >
+ <FontAwesomeIcon
+ icon={article.isRead ? faCheck : faCircle}
+ className="w-4 h-4"
+ />
+ </button>
+ )}
+ <div className="flex-1 min-w-0">
+ <button
+ type="button"
+ onClick={() => handleArticleClick(article)}
+ className={`text-left w-full group-hover:text-blue-600 transition-colors ${
+ article.isRead ? "text-gray-700" : "text-gray-900 font-medium"
+ }`}
+ >
+ <div className="flex items-center gap-2 break-words">
+ {article.title}
+ </div>
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/src/components/ArticleList.tsx b/frontend/src/components/ArticleList.tsx
index ee7b187..5d508c5 100644
--- a/frontend/src/components/ArticleList.tsx
+++ b/frontend/src/components/ArticleList.tsx
@@ -1,18 +1,8 @@
-import {
- faCheck,
- faCircle,
- faExternalLinkAlt,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useMutation } from "urql";
import type {
GetReadArticlesQuery,
GetUnreadArticlesQuery,
} from "../graphql/generated/graphql";
-import {
- MarkArticleReadDocument,
- MarkArticleUnreadDocument,
-} from "../graphql/generated/graphql";
+import { ArticleItem } from "./ArticleItem";
interface Props {
articles: NonNullable<
@@ -23,28 +13,6 @@ interface Props {
}
export function ArticleList({ articles, showReadStatus = true }: Props) {
- const [, markArticleRead] = useMutation(MarkArticleReadDocument);
- const [, markArticleUnread] = useMutation(MarkArticleUnreadDocument);
-
- const handleToggleRead = async (
- articleId: string,
- isCurrentlyRead: boolean,
- ) => {
- if (isCurrentlyRead) {
- await markArticleUnread({ id: articleId });
- } else {
- await markArticleRead({ id: articleId });
- }
- };
-
- const handleArticleClick = async (article: (typeof articles)[0]) => {
- // Open article in new tab and mark as read if it's unread
- window.open(article.url, "_blank");
- if (!article.isRead) {
- await markArticleRead({ id: article.id });
- }
- };
-
if (articles.length === 0) {
return (
<div className="p-4 text-center text-gray-500">No articles found.</div>
@@ -83,51 +51,11 @@ export function ArticleList({ articles, showReadStatus = true }: Props) {
</h3>
<div className="space-y-1">
{feedArticles.map((article) => (
- <div
+ <ArticleItem
key={article.id}
- className={`group flex items-center gap-3 rounded-lg border p-3 hover:bg-gray-50 ${
- article.isRead
- ? "border-gray-200 bg-white"
- : "border-blue-200 bg-blue-50"
- }`}
- >
- {showReadStatus && (
- <button
- type="button"
- onClick={() => handleToggleRead(article.id, article.isRead)}
- className={`flex-shrink-0 rounded p-1 transition-colors ${
- article.isRead
- ? "text-gray-400 hover:text-gray-600"
- : "text-blue-600 hover:text-blue-700"
- }`}
- title={article.isRead ? "Mark as unread" : "Mark as read"}
- >
- <FontAwesomeIcon
- icon={article.isRead ? faCheck : faCircle}
- className="w-4 h-4"
- />
- </button>
- )}
- <div className="flex-1 min-w-0">
- <button
- type="button"
- onClick={() => handleArticleClick(article)}
- className={`text-left w-full group-hover:text-blue-600 transition-colors ${
- article.isRead
- ? "text-gray-700"
- : "text-gray-900 font-medium"
- }`}
- >
- <div className="flex items-center gap-2">
- <span className="truncate">{article.title}</span>
- <FontAwesomeIcon
- icon={faExternalLinkAlt}
- className="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
- />
- </div>
- </button>
- </div>
- </div>
+ article={article}
+ showReadStatus={showReadStatus}
+ />
))}
</div>
</div>