aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/ArticleItem.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-13 15:58:47 +0900
committernsfisis <nsfisis@gmail.com>2025-07-13 15:58:47 +0900
commit43e7807034db67652bada00eee17a425d332b3f3 (patch)
treeef6a8b1271154011690cdb15406c52bd2f699df8 /frontend/src/components/ArticleItem.tsx
parent4082bdf34beda81815a41c8ce71086bed1c401cf (diff)
downloadfeedaka-43e7807034db67652bada00eee17a425d332b3f3.tar.gz
feedaka-43e7807034db67652bada00eee17a425d332b3f3.tar.zst
feedaka-43e7807034db67652bada00eee17a425d332b3f3.zip
refactor(frontend): extract ArticleItem from ArticleList
Diffstat (limited to 'frontend/src/components/ArticleItem.tsx')
-rw-r--r--frontend/src/components/ArticleItem.tsx86
1 files changed, 86 insertions, 0 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>
+ );
+}