feat: added infinite scroll to feed page

This commit is contained in:
MTG2000
2022-04-18 16:37:59 +03:00
parent 1a13cd6c3c
commit 1f817c8a79
12 changed files with 125 additions and 36 deletions

View File

@@ -1,19 +1,14 @@
import { useCallback } from "react"
import { Post } from "src/features/Posts/types"
import { useReachedBottom } from "src/utils/hooks/useReachedBottom"
import { ListProps } from "src/utils/interfaces"
import { ListComponentProps } from "src/utils/interfaces"
import PostCard, { PostCardSkeleton } from "../PostCard"
type Props = ListProps<Post>
type Props = ListComponentProps<Post>
export default function PostsList(props: Props) {
const reachedBottom = useCallback(() => {
console.log("NEW FETCH")
}, [])
const { ref } = useReachedBottom<HTMLDivElement>(reachedBottom)
const { ref } = useReachedBottom<HTMLDivElement>(props.onReachedBottom)
if (props.isLoading)
return <div className="flex flex-col gap-24">

View File

@@ -1,15 +1,22 @@
import { useFeedQuery } from 'src/graphql'
import { MOCK_DATA } from 'src/mocks/data'
import { useInfiniteQuery } from 'src/utils/hooks'
import PostsList from '../../Components/PostsList/PostsList'
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
import PopularCategories from './PopularCategories/PopularCategories'
import SortBy from './SortBy/SortBy'
import styles from './styles.module.css'
export default function FeedPage() {
const feedQuery = useFeedQuery()
const feedQuery = useFeedQuery({
variables: {
take: 10,
skip: 0
}
})
const { fetchMore, isFetchingMore } = useInfiniteQuery(feedQuery, 'getFeed')
return (
<div
@@ -22,7 +29,12 @@ export default function FeedPage() {
<PopularCategories />
</div>
</aside>
<PostsList isLoading={feedQuery.loading} items={feedQuery.data?.getFeed} />
<PostsList
isLoading={feedQuery.loading}
items={feedQuery.data?.getFeed}
isFetching={isFetchingMore}
onReachedBottom={fetchMore}
/>
<aside>
<div className="sticky top-16">
<TrendingCard />

View File

@@ -1,5 +1,5 @@
query Feed {
getFeed {
query Feed($skip: Int, $take: Int) {
getFeed(skip: $skip, take: $take) {
... on Story {
id
title

View File

@@ -254,7 +254,10 @@ export type SearchProjectsQueryVariables = Exact<{
export type SearchProjectsQuery = { __typename?: 'Query', searchProjects: Array<{ __typename?: 'Project', id: number, thumbnail_image: string, title: string, category: { __typename?: 'Category', title: string, id: number } }> };
export type FeedQueryVariables = Exact<{ [key: string]: never; }>;
export type FeedQueryVariables = Exact<{
skip: InputMaybe<Scalars['Int']>;
take: InputMaybe<Scalars['Int']>;
}>;
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, deadline: string, reward_amount: number, answers_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, date: string, body: string, author: { __typename?: 'User', id: number, name: string, image: string } }> } | { __typename?: 'Story', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, comments_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
@@ -391,8 +394,8 @@ export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQ
export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>;
export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>;
export const FeedDocument = gql`
query Feed {
getFeed {
query Feed($skip: Int, $take: Int) {
getFeed(skip: $skip, take: $take) {
... on Story {
id
title
@@ -480,6 +483,8 @@ export const FeedDocument = gql`
* @example
* const { data, loading, error } = useFeedQuery({
* variables: {
* skip: // value for 'skip'
* take: // value for 'take'
* },
* });
*/

View File

@@ -87,9 +87,7 @@ posts.bounties = posts.bounties.map(b => ({ ...b, __typename: "Bounty" }))
posts.questions = posts.questions.map(b => ({ ...b, __typename: "Question" }))
posts.stories = posts.stories.map(b => ({ ...b, __typename: "Story" }))
export const feed: Post[] = [
...posts.stories,
...posts.bounties,
...posts.questions,
]
export const feed: Post[] = Array(30).fill(0).map((_, idx) => {
return { ...posts.stories[0], id: idx + 1, title: `Post Title ${idx + 1}` }
})

View File

@@ -17,6 +17,7 @@ import {
FeedQuery,
PostDetailsQuery,
PostDetailsQueryVariables,
FeedQueryVariables,
} from 'src/graphql'
const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms))
@@ -99,12 +100,12 @@ export const handlers = [
)
}),
graphql.query<FeedQuery>('Feed', async (req, res, ctx) => {
graphql.query<FeedQuery, FeedQueryVariables>('Feed', async (req, res, ctx) => {
await delay()
const { take, skip } = req.variables;
return res(
ctx.data({
getFeed: getFeed()
getFeed: getFeed({ take, skip })
})
)
}),

View File

@@ -1,6 +1,6 @@
import ASSETS from "src/assets";
import { MOCK_DATA } from "./data";
import { Query } from 'src/graphql'
import { Query, QueryGetFeedArgs } from 'src/graphql'
export function getCategory(id: number) {
@@ -42,8 +42,10 @@ export function hottestProjects() {
return MOCK_DATA.projects.sort((p1, p2) => p2.votes_count - p1.votes_count).slice(0, 20)
}
export function getFeed(): Query['getFeed'] {
return MOCK_DATA.feed as any;
export function getFeed(config: QueryGetFeedArgs): Query['getFeed'] {
const take = config.take ?? 10
const skip = config.skip ?? 0
return MOCK_DATA.feed.slice(skip, skip + take) as any;
}
export function getPostById(postId: number): Query['getPostById'] {

View File

@@ -1,4 +1,4 @@
import { ApolloClient, HttpLink, InMemoryCache, from } from "@apollo/client";
import { ApolloClient, HttpLink, InMemoryCache, from, Reference, FieldPolicy } from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { RetryLink } from "@apollo/client/link/retry";
@@ -45,5 +45,40 @@ export const apolloClient = new ApolloClient({
errorLink,
httpLink
]),
cache: new InMemoryCache()
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getFeed: offsetLimitPagination()
},
},
},
})
});
type KeyArgs = FieldPolicy<any>["keyArgs"];
function offsetLimitPagination<T = Reference>(
keyArgs: KeyArgs = false,
): FieldPolicy<T[]> {
return {
keyArgs,
merge(existing, incoming, { args }) {
const merged = existing ? existing.slice(0) : [];
if (args) {
// Assume an skip of 0 if args.skip omitted.
const { skip = 0 } = args;
for (let i = 0; i < incoming.length; ++i) {
merged[skip + i] = incoming[i];
}
} else {
// It's unusual (probably a mistake) for a paginated field not
// to receive any arguments, so you might prefer to throw an
// exception here, instead of recovering by appending incoming
// onto the existing array.
merged.push.apply(merged, [...incoming]);
}
return merged;
},
};
}

View File

@@ -1,3 +1,5 @@
export * from "./storeHooks";
export * from "./useResizeListener";
export * from "./usePressHolder";
export * from "./useInfiniteQuery";
export * from "./useReachedBottom";

View File

@@ -0,0 +1,34 @@
import { QueryResult } from "@apollo/client";
import { useCallback, useState } from "react";
export const useInfiniteQuery = (query: QueryResult, dataField: string) => {
const [fetchingMore, setFetchingMore] = useState(false)
const [reachedLastPage, setReachedLastPage] = useState(false)
const fetchMore = useCallback(
() => {
if (!fetchingMore && !reachedLastPage) {
setFetchingMore(true);
// console.log(feedQuery.variables?.skip);
query.fetchMore({
variables: {
skip: query.data?.[dataField].length,
}
}).then((d) => {
if (d.data?.[dataField].length === 0)
setReachedLastPage(true)
setFetchingMore(false)
})
}
}
, [dataField, fetchingMore, query, reachedLastPage]
)
return {
isFetchingMore: fetchingMore,
fetchMore: fetchMore
}
}

View File

@@ -6,25 +6,30 @@ export const useReachedBottom = <T extends HTMLElement>(cb?: () => void, options
const { offset = window.innerHeight, throttle = 600 } = options
const ref = useRef<T>(null);
const callbackHandler = useRef<Function>();
useEffect(() => {
if (!cb)
callbackHandler.current = undefined;
else {
callbackHandler.current = _debounce(cb, throttle)
}
}, [cb, throttle])
useEffect(() => {
if (!cb) return;
const cbDebounced = _debounce(cb, throttle)
const listener = () => {
if (!ref.current) return;
const curWindowPosition = window.scrollY + window.innerHeight;
const elTriggerPosition = ref.current.offsetTop + ref.current.scrollHeight - offset;
if (curWindowPosition > elTriggerPosition) cbDebounced();
if (curWindowPosition > elTriggerPosition) callbackHandler.current?.();
}
document.addEventListener('scroll', listener)
return () => {
document.removeEventListener('scroll', listener)
}
}, [cb, offset, throttle])
}, [offset, throttle])
return { ref }
}

View File

@@ -5,7 +5,7 @@ export type Tag = {
}
export type ListProps<T> = {
export type ListComponentProps<T> = {
items?: T[]
isLoading?: boolean;
isFetching?: boolean;