aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/ArticleItem.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/ArticleItem.tsx')
-rw-r--r--frontend/src/components/ArticleItem.tsx35
1 files changed, 26 insertions, 9 deletions
diff --git a/frontend/src/components/ArticleItem.tsx b/frontend/src/components/ArticleItem.tsx
index e5a506f..8fb00b0 100644
--- a/frontend/src/components/ArticleItem.tsx
+++ b/frontend/src/components/ArticleItem.tsx
@@ -1,5 +1,6 @@
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useOptimistic } from "react";
import { useMutation } from "urql";
import type {
GetReadArticlesQuery,
@@ -24,10 +25,22 @@ export function ArticleItem({ article, showReadStatus = true }: Props) {
const [, markArticleRead] = useMutation(MarkArticleReadDocument);
const [, markArticleUnread] = useMutation(MarkArticleUnreadDocument);
+ const [optimisticArticle, setOptimisticArticle] = useOptimistic(
+ article,
+ (currentArticle, newReadState: boolean) => ({
+ ...currentArticle,
+ isRead: newReadState,
+ }),
+ );
+
const handleToggleRead = async (
articleId: string,
isCurrentlyRead: boolean,
) => {
+ const newReadState = !isCurrentlyRead;
+
+ setOptimisticArticle(newReadState);
+
if (isCurrentlyRead) {
await markArticleUnread({ id: articleId });
} else {
@@ -38,7 +51,9 @@ export function ArticleItem({ article, showReadStatus = true }: Props) {
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) {
+ if (!optimisticArticle.isRead) {
+ setOptimisticArticle(true);
+
await markArticleRead({ id: article.id });
}
};
@@ -46,7 +61,7 @@ export function ArticleItem({ article, showReadStatus = true }: Props) {
return (
<div
className={`group flex items-center gap-3 rounded-lg border p-3 hover:bg-gray-50 ${
- article.isRead
+ optimisticArticle.isRead
? "border-gray-200 bg-white"
: "border-blue-200 bg-blue-50"
}`}
@@ -54,16 +69,16 @@ export function ArticleItem({ article, showReadStatus = true }: Props) {
{showReadStatus && (
<button
type="button"
- onClick={() => handleToggleRead(article.id, article.isRead)}
+ onClick={() => handleToggleRead(article.id, optimisticArticle.isRead)}
className={`flex-shrink-0 rounded p-1 transition-colors ${
- article.isRead
+ optimisticArticle.isRead
? "text-gray-400 hover:text-gray-600"
: "text-blue-600 hover:text-blue-700"
}`}
- title={article.isRead ? "Mark as unread" : "Mark as read"}
+ title={optimisticArticle.isRead ? "Mark as unread" : "Mark as read"}
>
<FontAwesomeIcon
- icon={article.isRead ? faCheck : faCircle}
+ icon={optimisticArticle.isRead ? faCheck : faCircle}
className="w-4 h-4"
/>
</button>
@@ -71,13 +86,15 @@ export function ArticleItem({ article, showReadStatus = true }: Props) {
<div className="flex-1 min-w-0">
<button
type="button"
- onClick={() => handleArticleClick(article)}
+ onClick={() => handleArticleClick(optimisticArticle)}
className={`text-left w-full group-hover:text-blue-600 transition-colors ${
- article.isRead ? "text-gray-700" : "text-gray-900 font-medium"
+ optimisticArticle.isRead
+ ? "text-gray-700"
+ : "text-gray-900 font-medium"
}`}
>
<div className="flex items-center gap-2 break-words">
- {article.title}
+ {optimisticArticle.title}
</div>
</button>
</div>