1+ "use client" ;
2+
3+ import * as articleActions from "@/backend/services/article.actions" ;
4+ import ArticleCard from "@/components/ArticleCard" ;
5+ import VisibilitySensor from "@/components/VisibilitySensor" ;
6+ import { readingTime } from "@/lib/utils" ;
7+ import getFileUrl from "@/utils/getFileUrl" ;
8+ import { useInfiniteQuery } from "@tanstack/react-query" ;
9+ import { useMemo } from "react" ;
10+
11+ interface TagArticleFeedProps {
12+ tagId : string ;
13+ }
14+
15+ const TagArticleFeed : React . FC < TagArticleFeedProps > = ( { tagId } ) => {
16+ const tagFeedQuery = useInfiniteQuery ( {
17+ queryKey : [ "tag-articles" , tagId ] ,
18+ queryFn : ( { pageParam } ) =>
19+ articleActions . articlesByTag ( {
20+ tag_id : tagId ,
21+ limit : 5 ,
22+ page : pageParam ,
23+ } ) ,
24+ initialPageParam : 1 ,
25+ getNextPageParam : ( lastPage ) => {
26+ if ( ! lastPage ?. meta ?. hasNextPage ) return undefined ;
27+ const _page = lastPage ?. meta ?. currentPage ?? 1 ;
28+ return _page + 1 ;
29+ } ,
30+ } ) ;
31+
32+ const feedArticles = useMemo ( ( ) => {
33+ return tagFeedQuery . data ?. pages . flatMap ( ( page ) => page ?. nodes ) ?? [ ] ;
34+ } , [ tagFeedQuery . data ] ) ;
35+
36+ const totalArticles = useMemo ( ( ) => {
37+ return tagFeedQuery . data ?. pages ?. [ 0 ] ?. meta ?. total ?? 0 ;
38+ } , [ tagFeedQuery . data ] ) ;
39+
40+ const tagName = useMemo ( ( ) => {
41+ return tagFeedQuery . data ?. pages ?. [ 0 ] ?. tagName ?? "Unknown Tag" ;
42+ } , [ tagFeedQuery . data ] ) ;
43+
44+ // Show loading skeletons
45+ if ( tagFeedQuery . isPending ) {
46+ return (
47+ < div className = "flex flex-col gap-10 mt-2" >
48+ < div className = "h-56 bg-muted animate-pulse mx-4" />
49+ < div className = "h-56 bg-muted animate-pulse mx-4" />
50+ < div className = "h-56 bg-muted animate-pulse mx-4" />
51+ < div className = "h-56 bg-muted animate-pulse mx-4" />
52+ </ div >
53+ ) ;
54+ }
55+
56+ // Show error state
57+ if ( tagFeedQuery . isError ) {
58+ return (
59+ < div className = "flex flex-col items-center justify-center py-12" >
60+ < h2 className = "text-xl font-semibold text-gray-900 dark:text-white mb-2" >
61+ Error loading articles
62+ </ h2 >
63+ < p className = "text-gray-600 dark:text-gray-400 mb-4" >
64+ Failed to load articles for this tag.
65+ </ p >
66+ < button
67+ onClick = { ( ) => tagFeedQuery . refetch ( ) }
68+ className = "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
69+ >
70+ Try again
71+ </ button >
72+ </ div >
73+ ) ;
74+ }
75+
76+ // Show empty state
77+ if ( feedArticles . length === 0 ) {
78+ return (
79+ < div className = "flex flex-col items-center justify-center py-12" >
80+ < h2 className = "text-xl font-semibold text-gray-900 dark:text-white mb-2" >
81+ No articles found
82+ </ h2 >
83+ < p className = "text-gray-600 dark:text-gray-400" >
84+ No articles have been tagged with “{ tagName } ” yet.
85+ </ p >
86+ </ div >
87+ ) ;
88+ }
89+
90+ return (
91+ < >
92+ < div className = "mb-8" >
93+ < h1 className = "text-3xl font-bold text-gray-900 dark:text-white" >
94+ Articles tagged with “{ tagName } ”
95+ </ h1 >
96+ < p className = "text-gray-600 dark:text-gray-400 mt-2" >
97+ Found { totalArticles } articles
98+ </ p >
99+ </ div >
100+
101+ < div className = "flex flex-col gap-10 mt-2" >
102+ { feedArticles . map ( ( article ) => (
103+ < ArticleCard
104+ key = { article ?. id }
105+ id = { article ?. id ?. toString ( ) ?? "" }
106+ handle = { article ?. handle ?? "" }
107+ title = { article ?. title ?? "" }
108+ excerpt = { article ?. excerpt ?? "" }
109+ coverImage = { article ?. cover_image ? getFileUrl ( article . cover_image ) : "" }
110+ author = { {
111+ id : article ?. user ?. id ?? "" ,
112+ name : article ?. user ?. name ?? "" ,
113+ avatar : article ?. user ?. profile_photo
114+ ? getFileUrl ( article . user . profile_photo )
115+ : "" ,
116+ username : article ?. user ?. username ?? "" ,
117+ } }
118+ publishedAt = { article ?. created_at ?. toDateString ( ) ?? "" }
119+ readingTime = { readingTime ( article ?. body ?? "" ) }
120+ />
121+ ) ) }
122+
123+ < div className = "my-10" >
124+ < VisibilitySensor
125+ visible = { tagFeedQuery . hasNextPage }
126+ onLoadmore = { async ( ) => {
127+ await tagFeedQuery . fetchNextPage ( ) ;
128+ } }
129+ />
130+ </ div >
131+ </ div >
132+ </ >
133+ ) ;
134+ } ;
135+
136+ export default TagArticleFeed ;
0 commit comments