Skip to content

Commit b097c74

Browse files
committed
feat: implement infinite scrolling for AI articles and enhance feed components
1 parent a88a921 commit b097c74

10 files changed

Lines changed: 376 additions & 16 deletions

File tree

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,49 @@
1-
import { useQuery } from '@tanstack/react-query'
1+
import { useInfiniteQuery } from '@tanstack/react-query'
22
import { axios } from 'src/lib/axios'
3-
import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query'
4-
import { Article } from 'src/types'
3+
import { InfiniteQueryConfig } from 'src/lib/react-query'
4+
import { FeedItem } from 'src/types'
55

6-
const getAIArticles = async (userTopics: string[]): Promise<Article[]> => {
7-
return axios.get('/engine/feed/get', {
6+
type Response = {
7+
data: FeedItem[]
8+
metadata: {
9+
next: string | null
10+
hasNextPage: boolean
11+
}
12+
}
13+
const getAIArticles = async ({
14+
tags,
15+
next,
16+
}: {
17+
tags: string[]
18+
next?: string | null
19+
}): Promise<Response> => {
20+
return axios.get('/TO_ADD', {
21+
auth: {
22+
username: 'hidden',
23+
password: 'hidden',
24+
},
825
params: {
9-
tags: userTopics.join(','),
10-
limit: 50,
26+
tags: [...tags].sort().join(','),
27+
limit: 21,
28+
next,
1129
},
1230
})
1331
}
1432

1533
type QueryFnType = typeof getAIArticles
1634

1735
type UseGetArticlesOptions = {
18-
userTopics: string[]
19-
config?: QueryConfig<QueryFnType>
36+
tags: string[]
37+
config?: InfiniteQueryConfig<QueryFnType>
2038
}
2139

22-
export const useGetAIArticles = ({ userTopics, config }: UseGetArticlesOptions) => {
23-
return useQuery<ExtractFnReturnType<QueryFnType>>({
40+
export const useGetAIArticles = ({ tags, config }: UseGetArticlesOptions) => {
41+
return useInfiniteQuery<Response>({
2442
...config,
25-
queryKey: ['ai', userTopics.join(',')],
26-
queryFn: () => getAIArticles(userTopics),
43+
queryKey: ['feed', 'v2', tags.join(',')],
44+
queryFn: ({ pageParam }) => getAIArticles({ tags, next: pageParam }),
45+
getNextPageParam: (lastPage) => {
46+
return lastPage.metadata.hasNextPage ? JSON.stringify(lastPage.metadata.next) : undefined
47+
},
2748
})
2849
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './AICard'
2+
export * from './ArticleItem'

src/features/cards/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
export * from "./api/getRssFeed"
2-
export * from "./components/conferencesCard"
1+
export * from './api/getAIArticles'
2+
export * from './api/getRssFeed'
3+
export * from './components/aiCard'
4+
export * from './components/conferencesCard'
35
export * from './components/devtoCard'
46
export * from './components/freecodecampCard'
57
export * from './components/githubCard'
@@ -11,4 +13,3 @@ export * from './components/mediumCard'
1113
export * from './components/producthuntCard'
1214
export * from './components/redditCard'
1315
export * from './components/rssCard'
14-
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import useInfiniteScroll from 'react-infinite-scroll-hook'
2+
import { PropagateLoader } from 'react-spinners'
3+
import { useGetAIArticles } from 'src/features/cards'
4+
import { useUserPreferences } from 'src/stores/preferences'
5+
import './feed.css'
6+
import { FeedItem } from './FeedItem'
7+
import { AdvFeedItem } from './feedItems/AdvFeedItem'
8+
9+
export const Feed = () => {
10+
const { userSelectedTags } = useUserPreferences()
11+
const {
12+
data: feed,
13+
isLoading,
14+
isInitialLoading,
15+
hasNextPage,
16+
isError,
17+
error,
18+
isFetchingNextPage,
19+
fetchNextPage,
20+
} = useGetAIArticles({
21+
tags: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()),
22+
})
23+
24+
const [infiniteRef] = useInfiniteScroll({
25+
loading: isLoading,
26+
hasNextPage: Boolean(hasNextPage),
27+
onLoadMore: fetchNextPage,
28+
disabled: Boolean(error),
29+
rootMargin: '0px 0px 400px 0px',
30+
})
31+
32+
if (isInitialLoading) {
33+
return (
34+
<div className="feed">
35+
{Array.from({
36+
length: 10,
37+
}).map((_, index) => (
38+
<div className="feedItem placeholder" key={`loading-${index}`}>
39+
<div className="image"></div>
40+
<div className="line"></div>
41+
<div className="smallLine"></div>
42+
</div>
43+
))}
44+
</div>
45+
)
46+
}
47+
48+
return (
49+
<div className="feed">
50+
<div key={`adv`} className="feedItem">
51+
<AdvFeedItem />
52+
</div>
53+
{(feed?.pages.flatMap((page) => page.data) || []).map((article, index) => {
54+
return (
55+
<div key={article.id} className="feedItem">
56+
{/* TODO: fix analytics tag */}
57+
<FeedItem item={article} key={article.id} index={index} analyticsTag={'test'} />
58+
</div>
59+
)
60+
})}
61+
{hasNextPage && (
62+
<div className="loading" ref={infiniteRef}>
63+
<PropagateLoader color={'#A9B2BD'} loading={true} size={8} />
64+
</div>
65+
)}
66+
{isFetchingNextPage && isError && <div>Error while loading more pages</div>}
67+
</div>
68+
)
69+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { GoDotFill } from 'react-icons/go'
2+
import { MdAccessTime } from 'react-icons/md'
3+
import { CardItemWithActions } from 'src/components/Elements'
4+
import { useUserPreferences } from 'src/stores/preferences'
5+
import { BaseItemPropsType, FeedItem as FeedItemType } from 'src/types'
6+
import { format } from 'timeago.js'
7+
import { FeedItemHeader } from './FeedItemHeader'
8+
import { RepoFeedItem } from './feedItems/RepoFeedItem'
9+
10+
export const FeedItem = (props: BaseItemPropsType<FeedItemType>) => {
11+
const { item, index, analyticsTag } = props
12+
const { listingMode } = useUserPreferences()
13+
14+
if (item.type === 'github') {
15+
return <RepoFeedItem item={item} index={index} analyticsTag={analyticsTag} />
16+
}
17+
18+
return (
19+
<CardItemWithActions
20+
source={analyticsTag}
21+
index={index}
22+
item={item}
23+
key={index}
24+
cardItem={
25+
<>
26+
<FeedItemHeader item={item} />
27+
{listingMode === 'compact' && (
28+
<div className="rowDetails">
29+
<span className="rowItem capitalize">
30+
<GoDotFill className="rowItemIcon" /> {item.source}
31+
</span>
32+
</div>
33+
)}
34+
{listingMode === 'normal' && (
35+
<div className="rowDetails">
36+
<span className="rowItem capitalize">
37+
<GoDotFill className="rowItemIcon" /> {item.source}
38+
</span>
39+
<span className="rowItem" title={new Date(item.date).toUTCString()}>
40+
<MdAccessTime className="rowItemIcon" /> {format(new Date(item.date))}
41+
</span>
42+
</div>
43+
)}
44+
</>
45+
}
46+
/>
47+
)
48+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { CardLink } from 'src/components/Elements'
2+
import { Attributes } from 'src/lib/analytics'
3+
import { FeedItem as FeedItemType } from 'src/types'
4+
import { FeedItemImage } from './FeedItemImage'
5+
6+
type FeedItemHeaderProps = {
7+
item: FeedItemType
8+
fallbackImage?: string | React.ReactNode
9+
}
10+
11+
export const FeedItemHeader = ({ item, fallbackImage }: FeedItemHeaderProps) => {
12+
return (
13+
<div className="rowTitle">
14+
<CardLink
15+
link={item.url}
16+
className="titleWithCover"
17+
analyticsAttributes={{
18+
[Attributes.TRIGERED_FROM]: 'card',
19+
[Attributes.TITLE]: item.title,
20+
[Attributes.LINK]: item.url,
21+
[Attributes.SOURCE]: item.source,
22+
}}>
23+
<FeedItemImage imageUrl={item.image} fallbackImage={fallbackImage} />
24+
<span className="subTitle">{item.title}</span>
25+
</CardLink>
26+
</div>
27+
)
28+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useState } from 'react'
2+
import { BiNews } from 'react-icons/bi'
3+
4+
type FeedItemImageProps = {
5+
imageUrl?: string
6+
fallbackImage?: string | React.ReactNode
7+
}
8+
9+
export const FeedItemImage = ({ imageUrl, fallbackImage }: FeedItemImageProps) => {
10+
const [hasError, setHasError] = useState(false)
11+
12+
if (hasError || !imageUrl) {
13+
if (typeof fallbackImage === 'string') {
14+
return <img src={fallbackImage} className="rowCover" alt="" />
15+
} else {
16+
return (
17+
fallbackImage || (
18+
<div className="rowCover placeholder">
19+
<BiNews size={52} />
20+
<span>Cover image not found</span>
21+
</div>
22+
)
23+
)
24+
}
25+
}
26+
27+
return (
28+
<img
29+
src={imageUrl}
30+
className="rowCover"
31+
alt=""
32+
onError={(e) => {
33+
e.currentTarget.onerror = null
34+
setHasError(true)
35+
}}
36+
/>
37+
)
38+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
.light .feedItem {
2+
box-shadow: 0 0 12px var(--card-border-color);
3+
}
4+
.feed {
5+
display: grid;
6+
grid-template-columns: repeat(3, 1fr);
7+
scroll-margin-top: 12px;
8+
gap: 16px;
9+
10+
@media (max-width: 768px) {
11+
display: flex;
12+
flex-direction: column;
13+
}
14+
15+
.feedItem {
16+
display: flex;
17+
background-color: var(--card-background-color);
18+
border: 1px solid var(--card-border-color);
19+
border-radius: 10px;
20+
min-height: 300px;
21+
}
22+
.feedItem .repo {
23+
display: flex;
24+
flex-direction: column;
25+
gap: 12px;
26+
background: white;
27+
border-bottom: 12px solid green;
28+
}
29+
.feedItem .repo .title {
30+
font-size: 24px;
31+
color: black;
32+
margin: 0;
33+
padding: 0;
34+
}
35+
.feedItem .repo .description {
36+
font-size: 24px;
37+
color: black;
38+
margin: 0;
39+
padding: 0;
40+
}
41+
.loading {
42+
display: flex;
43+
flex-direction: column;
44+
align-items: center;
45+
justify-content: center;
46+
grid-column: span 3;
47+
padding: 16px 0;
48+
margin-bottom: 20px;
49+
}
50+
.blockRow {
51+
padding: 16px;
52+
width: 100%;
53+
}
54+
#banneradv .wrap {
55+
display: flex;
56+
flex-direction: column;
57+
align-items: center;
58+
justify-content: center;
59+
}
60+
}
61+
62+
.feed .placeholder {
63+
animation-duration: 1.5s;
64+
animation-name: cardPlaceholderPulse;
65+
animation-iteration-count: infinite;
66+
padding: 12px;
67+
display: flex;
68+
flex-direction: column;
69+
70+
gap: 16px;
71+
.image {
72+
background-color: var(--placeholder-background-color);
73+
border: 1px solid var(--placeholder-border-color);
74+
height: 70%;
75+
width: 100%;
76+
}
77+
.line {
78+
background: var(--placeholder-background-color);
79+
display: block;
80+
height: 17px;
81+
width: 100%;
82+
border-radius: 4px;
83+
margin: 0;
84+
}
85+
.smallLine {
86+
margin-top: 8px;
87+
background: var(--placeholder-background-color);
88+
display: block;
89+
height: 12px;
90+
width: 100%;
91+
border-radius: 4px;
92+
margin: 0;
93+
}
94+
}

0 commit comments

Comments
 (0)