1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
import { useState } from "react";
import type {
GetReadArticlesQuery,
GetUnreadArticlesQuery,
} from "../graphql/generated/graphql";
import { ArticleItem } from "./ArticleItem";
type ArticleType =
| GetUnreadArticlesQuery["unreadArticles"]["articles"]
| GetReadArticlesQuery["readArticles"]["articles"];
interface Props {
articles: ArticleType;
isReadView?: boolean;
isSingleFeed?: boolean;
hasNextPage?: boolean;
loadingMore?: boolean;
onLoadMore?: () => void;
}
export function ArticleList({
articles,
isReadView,
isSingleFeed,
hasNextPage,
loadingMore,
onLoadMore,
}: Props) {
const [hiddenArticleIds, setHiddenArticleIds] = useState<Set<string>>(
new Set(),
);
const handleArticleReadChange = (articleId: string, isRead: boolean) => {
if (isReadView !== isRead) {
setHiddenArticleIds((prev) => new Set(prev).add(articleId));
}
};
const visibleArticles = articles.filter(
(article) => !hiddenArticleIds.has(article.id),
);
if (visibleArticles.length === 0) {
return (
<div className="py-8 text-center">
<p className="text-sm text-stone-400">No articles found.</p>
</div>
);
}
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) => {
const feedId = article.feed.id;
if (!acc[feedId]) {
acc[feedId] = {
feed: article.feed,
articles: [],
};
}
acc[feedId].articles.push(article);
return acc;
},
{} as Record<
string,
{ feed: { id: string; title: string }; articles: typeof articles }
>,
);
return (
<div className="space-y-8">
{Object.values(articlesByFeed).map(({ feed, articles: feedArticles }) => (
<div key={feed.id} className="space-y-3">
<h3 className="border-b border-stone-200 pb-2 text-sm font-semibold uppercase tracking-wide text-stone-900">
{feed.title}
<span className="ml-2 text-xs font-normal normal-case tracking-normal text-stone-400">
{feedArticles.length} article
{feedArticles.length !== 1 ? "s" : ""}
</span>
</h3>
<div className="space-y-1">
{feedArticles.map((article) => (
<ArticleItem
key={article.id}
article={article}
onReadChange={handleArticleReadChange}
/>
))}
</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>
);
}
|