diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-07-13 15:58:47 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-07-13 15:58:47 +0900 |
| commit | 43e7807034db67652bada00eee17a425d332b3f3 (patch) | |
| tree | ef6a8b1271154011690cdb15406c52bd2f699df8 /frontend/src/components/ArticleItem.tsx | |
| parent | 4082bdf34beda81815a41c8ce71086bed1c401cf (diff) | |
| download | feedaka-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.tsx | 86 |
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> + ); +} |
